aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoe Chen <jc@unknwon.io>2023-02-07 23:39:00 +0800
committerGitHub <noreply@github.com>2023-02-07 23:39:00 +0800
commit133b9d90441008ee175e1f8e6369e06309e1392a (patch)
tree70f29798055962f6700ba6a93b023a8328a5eff4
parent7c453d5b3632a6bbdbd99205c518303a9e25a4e1 (diff)
refactor(db): finish migrate methods off `user.go` (#7337)
-rw-r--r--internal/cmd/admin.go4
-rw-r--r--internal/conf/mocks.go8
-rw-r--r--internal/db/actions.go20
-rw-r--r--internal/db/error.go33
-rw-r--r--internal/db/follows.go4
-rw-r--r--internal/db/issue.go58
-rw-r--r--internal/db/org.go20
-rw-r--r--internal/db/org_users_test.go2
-rw-r--r--internal/db/orgs_test.go2
-rw-r--r--internal/db/public_keys.go103
-rw-r--r--internal/db/public_keys_test.go69
-rw-r--r--internal/db/repo.go21
-rw-r--r--internal/db/repo_collaboration.go8
-rw-r--r--internal/db/repos.go91
-rw-r--r--internal/db/repos_test.go41
-rw-r--r--internal/db/ssh_key.go4
-rw-r--r--internal/db/user.go197
-rw-r--r--internal/db/users.go227
-rw-r--r--internal/db/users_test.go268
-rw-r--r--internal/db/watches.go39
-rw-r--r--internal/db/watches_test.go47
-rw-r--r--internal/route/admin/admin.go2
-rw-r--r--internal/route/admin/users.go2
-rw-r--r--internal/route/api/v1/admin/user.go2
-rw-r--r--internal/route/lfs/mocks_test.go487
-rw-r--r--internal/route/user/setting.go2
26 files changed, 1451 insertions, 310 deletions
diff --git a/internal/cmd/admin.go b/internal/cmd/admin.go
index 54a4a254..8f40bf1c 100644
--- a/internal/cmd/admin.go
+++ b/internal/cmd/admin.go
@@ -52,8 +52,8 @@ to make automatic initialization process more smoothly`,
Name: "delete-inactive-users",
Usage: "Delete all inactive accounts",
Action: adminDashboardOperation(
- db.DeleteInactivateUsers,
- "All inactivate accounts have been deleted successfully",
+ func() error { return db.Users.DeleteInactivated() },
+ "All inactivated accounts have been deleted successfully",
),
Flags: []cli.Flag{
stringFlag("config, c", "", "Custom configuration file path"),
diff --git a/internal/conf/mocks.go b/internal/conf/mocks.go
index 51d1fb14..efdd3aeb 100644
--- a/internal/conf/mocks.go
+++ b/internal/conf/mocks.go
@@ -37,11 +37,15 @@ func SetMockServer(t *testing.T, opts ServerOpts) {
})
}
+var mockSSH sync.Mutex
+
func SetMockSSH(t *testing.T, opts SSHOpts) {
+ mockSSH.Lock()
before := SSH
SSH = opts
t.Cleanup(func() {
SSH = before
+ mockSSH.Unlock()
})
}
@@ -65,10 +69,14 @@ func SetMockUI(t *testing.T, opts UIOpts) {
})
}
+var mockPicture sync.Mutex
+
func SetMockPicture(t *testing.T, opts PictureOpts) {
+ mockPicture.Lock()
before := Picture
Picture = opts
t.Cleanup(func() {
Picture = before
+ mockPicture.Unlock()
})
}
diff --git a/internal/db/actions.go b/internal/db/actions.go
index 74c15291..72d3b6ac 100644
--- a/internal/db/actions.go
+++ b/internal/db/actions.go
@@ -111,16 +111,16 @@ func (db *actions) listByOrganization(ctx context.Context, orgID, actorID, after
Where("?", afterID <= 0).
Or("id < ?", afterID),
).
- Where("repo_id IN (?)",
- db.Select("repository.id").
- Table("repository").
- Joins("JOIN team_repo ON repository.id = team_repo.repo_id").
- Where("team_repo.team_id IN (?)",
- db.Select("team_id").
- Table("team_user").
- Where("team_user.org_id = ? AND uid = ?", orgID, actorID),
- ).
- Or("repository.is_private = ? AND repository.is_unlisted = ?", false, false),
+ Where("repo_id IN (?)", db.
+ Select("repository.id").
+ Table("repository").
+ Joins("JOIN team_repo ON repository.id = team_repo.repo_id").
+ Where("team_repo.team_id IN (?)", db.
+ Select("team_id").
+ Table("team_user").
+ Where("team_user.org_id = ? AND uid = ?", orgID, actorID),
+ ).
+ Or("repository.is_private = ? AND repository.is_unlisted = ?", false, false),
).
Limit(conf.UI.User.NewsFeedPagingNum).
Order("id DESC")
diff --git a/internal/db/error.go b/internal/db/error.go
index f754df6d..8436aa99 100644
--- a/internal/db/error.go
+++ b/internal/db/error.go
@@ -8,39 +8,6 @@ import (
"fmt"
)
-// ____ ___
-// | | \______ ___________
-// | | / ___// __ \_ __ \
-// | | /\___ \\ ___/| | \/
-// |______//____ >\___ >__|
-// \/ \/
-
-type ErrUserOwnRepos struct {
- UID int64
-}
-
-func IsErrUserOwnRepos(err error) bool {
- _, ok := err.(ErrUserOwnRepos)
- return ok
-}
-
-func (err ErrUserOwnRepos) Error() string {
- return fmt.Sprintf("user still has ownership of repositories [uid: %d]", err.UID)
-}
-
-type ErrUserHasOrgs struct {
- UID int64
-}
-
-func IsErrUserHasOrgs(err error) bool {
- _, ok := err.(ErrUserHasOrgs)
- return ok
-}
-
-func (err ErrUserHasOrgs) Error() string {
- return fmt.Sprintf("user still has membership of organizations [uid: %d]", err.UID)
-}
-
// __ __.__ __ .__
// / \ / \__| | _|__|
// \ \/\/ / | |/ / |
diff --git a/internal/db/follows.go b/internal/db/follows.go
index 83f37e2e..bf50042a 100644
--- a/internal/db/follows.go
+++ b/internal/db/follows.go
@@ -55,7 +55,7 @@ func (*follows) updateFollowingCount(tx *gorm.DB, userID, followID int64) error
).
Error
if err != nil {
- return errors.Wrap(err, `update "num_followers"`)
+ return errors.Wrap(err, `update "user.num_followers"`)
}
/*
@@ -75,7 +75,7 @@ func (*follows) updateFollowingCount(tx *gorm.DB, userID, followID int64) error
).
Error
if err != nil {
- return errors.Wrap(err, `update "num_following"`)
+ return errors.Wrap(err, `update "user.num_following"`)
}
return nil
}
diff --git a/internal/db/issue.go b/internal/db/issue.go
index 3d87d795..74bf837a 100644
--- a/internal/db/issue.go
+++ b/internal/db/issue.go
@@ -26,36 +26,36 @@ var ErrMissingIssueNumber = errors.New("No issue number specified")
// Issue represents an issue or pull request of repository.
type Issue struct {
- ID int64
- RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
- Repo *Repository `xorm:"-" json:"-"`
- Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
- PosterID int64
- Poster *User `xorm:"-" json:"-"`
- Title string `xorm:"name"`
- Content string `xorm:"TEXT"`
- RenderedContent string `xorm:"-" json:"-"`
- Labels []*Label `xorm:"-" json:"-"`
- MilestoneID int64
- Milestone *Milestone `xorm:"-" json:"-"`
+ ID int64 `gorm:"primaryKey"`
+ RepoID int64 `xorm:"INDEX UNIQUE(repo_index)" gorm:"index;uniqueIndex:issue_repo_index_unique;not null"`
+ Repo *Repository `xorm:"-" json:"-" gorm:"-"`
+ Index int64 `xorm:"UNIQUE(repo_index)" gorm:"uniqueIndex:issue_repo_index_unique;not null"` // Index in one repository.
+ PosterID int64 `gorm:"index"`
+ Poster *User `xorm:"-" json:"-" gorm:"-"`
+ Title string `xorm:"name" gorm:"name"`
+ Content string `xorm:"TEXT" gorm:"type:TEXT"`
+ RenderedContent string `xorm:"-" json:"-" gorm:"-"`
+ Labels []*Label `xorm:"-" json:"-" gorm:"-"`
+ MilestoneID int64 `gorm:"index"`
+ Milestone *Milestone `xorm:"-" json:"-" gorm:"-"`
Priority int
- AssigneeID int64
- Assignee *User `xorm:"-" json:"-"`
+ AssigneeID int64 `gorm:"index"`
+ Assignee *User `xorm:"-" json:"-" gorm:"-"`
IsClosed bool
- IsRead bool `xorm:"-" json:"-"`
+ IsRead bool `xorm:"-" json:"-" gorm:"-"`
IsPull bool // Indicates whether is a pull request or not.
- PullRequest *PullRequest `xorm:"-" json:"-"`
+ PullRequest *PullRequest `xorm:"-" json:"-" gorm:"-"`
NumComments int
- Deadline time.Time `xorm:"-" json:"-"`
+ Deadline time.Time `xorm:"-" json:"-" gorm:"-"`
DeadlineUnix int64
- Created time.Time `xorm:"-" json:"-"`
+ Created time.Time `xorm:"-" json:"-" gorm:"-"`
CreatedUnix int64
- Updated time.Time `xorm:"-" json:"-"`
+ Updated time.Time `xorm:"-" json:"-" gorm:"-"`
UpdatedUnix int64
- Attachments []*Attachment `xorm:"-" json:"-"`
- Comments []*Comment `xorm:"-" json:"-"`
+ Attachments []*Attachment `xorm:"-" json:"-" gorm:"-"`
+ Comments []*Comment `xorm:"-" json:"-" gorm:"-"`
}
func (issue *Issue) BeforeInsert() {
@@ -1036,10 +1036,10 @@ func GetParticipantsByIssueID(issueID int64) ([]*User, error) {
// IssueUser represents an issue-user relation.
type IssueUser struct {
- ID int64
- UID int64 `xorm:"INDEX"` // User ID.
+ ID int64 `gorm:"primary_key"`
+ UserID int64 `xorm:"uid INDEX" gorm:"column:uid;index"`
IssueID int64
- RepoID int64 `xorm:"INDEX"`
+ RepoID int64 `xorm:"INDEX" gorm:"index"`
MilestoneID int64
IsRead bool
IsAssigned bool
@@ -1065,7 +1065,7 @@ func newIssueUsers(e *xorm.Session, repo *Repository, issue *Issue) error {
issueUsers = append(issueUsers, &IssueUser{
IssueID: issue.ID,
RepoID: repo.ID,
- UID: assignee.ID,
+ UserID: assignee.ID,
IsPoster: isPoster,
IsAssigned: assignee.ID == issue.AssigneeID,
})
@@ -1077,7 +1077,7 @@ func newIssueUsers(e *xorm.Session, repo *Repository, issue *Issue) error {
issueUsers = append(issueUsers, &IssueUser{
IssueID: issue.ID,
RepoID: repo.ID,
- UID: issue.PosterID,
+ UserID: issue.PosterID,
IsPoster: true,
})
}
@@ -1107,7 +1107,7 @@ func NewIssueUsers(repo *Repository, issue *Issue) (err error) {
func PairsContains(ius []*IssueUser, issueId, uid int64) int {
for i := range ius {
if ius[i].IssueID == issueId &&
- ius[i].UID == uid {
+ ius[i].UserID == uid {
return i
}
}
@@ -1117,7 +1117,7 @@ func PairsContains(ius []*IssueUser, issueId, uid int64) int {
// GetIssueUsers returns issue-user pairs by given repository and user.
func GetIssueUsers(rid, uid int64, isClosed bool) ([]*IssueUser, error) {
ius := make([]*IssueUser, 0, 10)
- err := x.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoID: rid, UID: uid})
+ err := x.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoID: rid, UserID: uid})
return ius, err
}
@@ -1442,7 +1442,7 @@ func UpdateIssueUserByRead(uid, issueID int64) error {
func updateIssueUsersByMentions(e Engine, issueID int64, uids []int64) error {
for _, uid := range uids {
iu := &IssueUser{
- UID: uid,
+ UserID: uid,
IssueID: issueID,
}
has, err := e.Get(iu)
diff --git a/internal/db/org.go b/internal/db/org.go
index cb68451a..8bc16701 100644
--- a/internal/db/org.go
+++ b/internal/db/org.go
@@ -204,9 +204,20 @@ func Organizations(page, pageSize int) ([]*User, error) {
return orgs, x.Limit(pageSize, (page-1)*pageSize).Where("type=1").Asc("id").Find(&orgs)
}
+// deleteBeans deletes all given beans, beans should contain delete conditions.
+func deleteBeans(e Engine, beans ...any) (err error) {
+ for i := range beans {
+ if _, err = e.Delete(beans[i]); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
// DeleteOrganization completely and permanently deletes everything of organization.
-func DeleteOrganization(org *User) (err error) {
- if err := DeleteUser(org); err != nil {
+func DeleteOrganization(org *User) error {
+ err := Users.DeleteByID(context.TODO(), org.ID, false)
+ if err != nil {
return err
}
@@ -223,11 +234,6 @@ func DeleteOrganization(org *User) (err error) {
); err != nil {
return fmt.Errorf("deleteBeans: %v", err)
}
-
- if err = deleteUser(sess, org); err != nil {
- return fmt.Errorf("deleteUser: %v", err)
- }
-
return sess.Commit()
}
diff --git a/internal/db/org_users_test.go b/internal/db/org_users_test.go
index 49f238d3..bff515bc 100644
--- a/internal/db/org_users_test.go
+++ b/internal/db/org_users_test.go
@@ -47,7 +47,7 @@ func TestOrgUsers(t *testing.T) {
func orgUsersCountByUser(t *testing.T, db *orgUsers) {
ctx := context.Background()
- // TODO: Use OrgUsers.Join to replace SQL hack when the method is available.
+ // TODO: Use Orgs.Join to replace SQL hack when the method is available.
err := db.Exec(`INSERT INTO org_user (uid, org_id) VALUES (?, ?)`, 1, 1).Error
require.NoError(t, err)
err = db.Exec(`INSERT INTO org_user (uid, org_id) VALUES (?, ?)`, 2, 1).Error
diff --git a/internal/db/orgs_test.go b/internal/db/orgs_test.go
index ecc4cfcd..9989394d 100644
--- a/internal/db/orgs_test.go
+++ b/internal/db/orgs_test.go
@@ -66,7 +66,7 @@ func orgsList(t *testing.T, db *orgs) {
).Error
require.NoError(t, err)
- // TODO: Use OrgUsers.Join to replace SQL hack when the method is available.
+ // TODO: Use Orgs.Join to replace SQL hack when the method is available.
err = db.Exec(`INSERT INTO org_user (uid, org_id, is_public) VALUES (?, ?, ?)`, alice.ID, org1.ID, false).Error
require.NoError(t, err)
err = db.Exec(`INSERT INTO org_user (uid, org_id, is_public) VALUES (?, ?, ?)`, alice.ID, org2.ID, true).Error
diff --git a/internal/db/public_keys.go b/internal/db/public_keys.go
new file mode 100644
index 00000000..d2f8307d
--- /dev/null
+++ b/internal/db/public_keys.go
@@ -0,0 +1,103 @@
+// Copyright 2023 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package db
+
+import (
+ "os"
+ "path/filepath"
+
+ "github.com/pkg/errors"
+ "gorm.io/gorm"
+
+ "gogs.io/gogs/internal/conf"
+ "gogs.io/gogs/internal/osutil"
+)
+
+// PublicKeysStore is the persistent interface for public keys.
+//
+// NOTE: All methods are sorted in alphabetical order.
+type PublicKeysStore interface {
+ // RewriteAuthorizedKeys rewrites the "authorized_keys" file under the SSH root
+ // path with all public keys stored in the database.
+ RewriteAuthorizedKeys() error
+}
+
+var PublicKeys PublicKeysStore
+
+var _ PublicKeysStore = (*publicKeys)(nil)
+
+type publicKeys struct {
+ *gorm.DB
+}
+
+// NewPublicKeysStore returns a persistent interface for public keys with given
+// database connection.
+func NewPublicKeysStore(db *gorm.DB) PublicKeysStore {
+ return &publicKeys{DB: db}
+}
+
+func authorizedKeysPath() string {
+ return filepath.Join(conf.SSH.RootPath, "authorized_keys")
+}
+
+func (db *publicKeys) RewriteAuthorizedKeys() error {
+ sshOpLocker.Lock()
+ defer sshOpLocker.Unlock()
+
+ err := os.MkdirAll(conf.SSH.RootPath, os.ModePerm)
+ if err != nil {
+ return errors.Wrap(err, "create SSH root path")
+ }
+ fpath := authorizedKeysPath()
+ tempPath := fpath + ".tmp"
+ f, err := os.OpenFile(tempPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
+ if err != nil {
+ return errors.Wrap(err, "create temporary file")
+ }
+ defer func() {
+ _ = f.Close()
+ _ = os.Remove(tempPath)
+ }()
+
+ // NOTE: More recently updated keys are more likely to be used more frequently,
+ // putting them in the earlier lines could speed up the key lookup by SSHD.
+ rows, err := db.Model(&PublicKey{}).Order("updated_unix DESC").Rows()
+ if err != nil {
+ return errors.Wrap(err, "iterate public keys")
+ }
+ defer func() { _ = rows.Close() }()
+
+ for rows.Next() {
+ var key PublicKey
+ err = db.ScanRows(rows, &key)
+ if err != nil {
+ return errors.Wrap(err, "scan rows")
+ }
+
+ _, err = f.WriteString(key.AuthorizedString())
+ if err != nil {
+ return errors.Wrapf(err, "write key %d", key.ID)
+ }
+ }
+ if err = rows.Err(); err != nil {
+ return errors.Wrap(err, "check rows.Err")
+ }
+
+ err = f.Close()
+ if err != nil {
+ return errors.Wrap(err, "close temporary file")
+ }
+ if osutil.IsExist(fpath) {
+ err = os.Remove(fpath)
+ if err != nil {
+ return errors.Wrap(err, "remove")
+ }
+ }
+ err = os.Rename(tempPath, fpath)
+ if err != nil {
+ return errors.Wrap(err, "rename")
+ }
+ return nil
+}
diff --git a/internal/db/public_keys_test.go b/internal/db/public_keys_test.go
new file mode 100644
index 00000000..1e83014b
--- /dev/null
+++ b/internal/db/public_keys_test.go
@@ -0,0 +1,69 @@
+// Copyright 2023 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package db
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "gogs.io/gogs/internal/conf"
+ "gogs.io/gogs/internal/dbtest"
+)
+
+func TestPublicKeys(t *testing.T) {
+ if testing.Short() {
+ t.Skip()
+ }
+ t.Parallel()
+
+ tables := []any{new(PublicKey)}
+ db := &publicKeys{
+ DB: dbtest.NewDB(t, "publicKeys", tables...),
+ }
+
+ for _, tc := range []struct {
+ name string
+ test func(t *testing.T, db *publicKeys)
+ }{
+ {"RewriteAuthorizedKeys", publicKeysRewriteAuthorizedKeys},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Cleanup(func() {
+ err := clearTables(t, db.DB, tables...)
+ require.NoError(t, err)
+ })
+ tc.test(t, db)
+ })
+ if t.Failed() {
+ break
+ }
+ }
+}
+
+func publicKeysRewriteAuthorizedKeys(t *testing.T, db *publicKeys) {
+ // TODO: Use PublicKeys.Add to replace SQL hack when the method is available.
+ publicKey := &PublicKey{
+ OwnerID: 1,
+ Name: "test-key",
+ Fingerprint: "12:f8:7e:78:61:b4:bf:e2:de:24:15:96:4e:d4:72:53",
+ Content: "test-key-content",
+ }
+ err := db.DB.Create(publicKey).Error
+ require.NoError(t, err)
+ tempSSHRootPath := filepath.Join(os.TempDir(), "publicKeysRewriteAuthorizedKeys-tempSSHRootPath")
+ conf.SetMockSSH(t, conf.SSHOpts{RootPath: tempSSHRootPath})
+ err = db.RewriteAuthorizedKeys()
+ require.NoError(t, err)
+
+ authorizedKeys, err := os.ReadFile(authorizedKeysPath())
+ require.NoError(t, err)
+ assert.Contains(t, string(authorizedKeys), fmt.Sprintf("key-%d", publicKey.ID))
+ assert.Contains(t, string(authorizedKeys), publicKey.Content)
+}
diff --git a/internal/db/repo.go b/internal/db/repo.go
index 27b61df2..cc73ae29 100644
--- a/internal/db/repo.go
+++ b/internal/db/repo.go
@@ -1839,15 +1839,6 @@ func GetUserAndCollaborativeRepositories(userID int64) ([]*Repository, error) {
return append(repos, ownRepos...), nil
}
-func getRepositoryCount(_ Engine, u *User) (int64, error) {
- return x.Count(&Repository{OwnerID: u.ID})
-}
-
-// GetRepositoryCount returns the total number of repositories of user.
-func GetRepositoryCount(u *User) (int64, error) {
- return getRepositoryCount(x, u)
-}
-
type SearchRepoOptions struct {
Keyword string
OwnerID int64
@@ -2362,6 +2353,8 @@ func watchRepo(e Engine, userID, repoID int64, watch bool) (err error) {
}
// Watch or unwatch repository.
+//
+// Deprecated: Use Watches.Watch instead.
func WatchRepo(userID, repoID int64, watch bool) (err error) {
return watchRepo(x, userID, repoID, watch)
}
@@ -2441,18 +2434,20 @@ func NotifyWatchers(act *Action) error {
// \/ \/
type Star struct {
- ID int64
- UID int64 `xorm:"UNIQUE(s)"`
- RepoID int64 `xorm:"UNIQUE(s)"`
+ ID int64 `gorm:"primaryKey"`
+ UserID int64 `xorm:"uid UNIQUE(s)" gorm:"column:uid;uniqueIndex:star_user_repo_unique;not null"`
+ RepoID int64 `xorm:"UNIQUE(s)" gorm:"uniqueIndex:star_user_repo_unique;not null"`
}
// Star or unstar repository.
+//
+// Deprecated: Use Stars.Star instead.
func StarRepo(userID, repoID int64, star bool) (err error) {
if star {
if IsStaring(userID, repoID) {
return nil
}
- if _, err = x.Insert(&Star{UID: userID, RepoID: repoID}); err != nil {
+ if _, err = x.Insert(&Star{UserID: userID, RepoID: repoID}); err != nil {
return err
} else if _, err = x.Exec("UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repoID); err != nil {
return err
diff --git a/internal/db/repo_collaboration.go b/internal/db/repo_collaboration.go
index 8aec18dc..758b824b 100644
--- a/internal/db/repo_collaboration.go
+++ b/internal/db/repo_collaboration.go
@@ -14,10 +14,10 @@ import (
// Collaboration represent the relation between an individual and a repository.
type Collaboration struct {
- ID int64
- RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
- UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
- Mode AccessMode `xorm:"DEFAULT 2 NOT NULL"`
+ ID int64 `gorm:"primary_key"`
+ UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL" gorm:"uniqueIndex:collaboration_user_repo_unique;index;not null"`
+ RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL" gorm:"uniqueIndex:collaboration_user_repo_unique;index;not null"`
+ Mode AccessMode `xorm:"DEFAULT 2 NOT NULL" gorm:"not null;default:2"`
}
func (c *Collaboration) ModeI18nKey() string {
diff --git a/internal/db/repos.go b/internal/db/repos.go
index 10f292b9..7791a458 100644
--- a/internal/db/repos.go
+++ b/internal/db/repos.go
@@ -11,6 +11,7 @@ import (
"time"
api "github.com/gogs/go-gogs-client"
+ "github.com/pkg/errors"
"gorm.io/gorm"
"gogs.io/gogs/internal/errutil"
@@ -36,9 +37,14 @@ type ReposStore interface {
// Repositories that are owned directly by the given collaborator are not
// included.
GetByCollaboratorIDWithAccessMode(ctx context.Context, collaboratorID int64) (map[*Repository]AccessMode, error)
+ // GetByID returns the repository with given ID. It returns ErrRepoNotExist when
+ // not found.
+ GetByID(ctx context.Context, id int64) (*Repository, error)
// GetByName returns the repository with given owner and name. It returns
// ErrRepoNotExist when not found.
GetByName(ctx context.Context, ownerID int64, name string) (*Repository, error)
+ // Star marks the user to star the repository.
+ Star(ctx context.Context, userID, repoID int64) error
// Touch updates the updated time to the current time and removes the bare state
// of the given repository.
Touch(ctx context.Context, id int64) error
@@ -177,7 +183,18 @@ func (db *repos) Create(ctx context.Context, ownerID int64, opts CreateRepoOptio
IsFork: opts.Fork,
ForkID: opts.ForkID,
}
- return repo, db.WithContext(ctx).Create(repo).Error
+ return repo, db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ err = tx.Create(repo).Error
+ if err != nil {
+ return errors.Wrap(err, "create")
+ }
+
+ err = NewWatchesStore(tx).Watch(ctx, ownerID, repo.ID)
+ if err != nil {
+ return errors.Wrap(err, "watch")
+ }
+ return nil
+ })
}
func (db *repos) GetByCollaboratorID(ctx context.Context, collaboratorID int64, limit int, orderBy string) ([]*Repository, error) {
@@ -252,6 +269,18 @@ func (ErrRepoNotExist) NotFound() bool {
return true
}
+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 {
+ return nil, ErrRepoNotExist{errutil.Args{"repoID": id}}
+ }
+ return nil, err
+ }
+ return repo, nil
+}
+
func (db *repos) GetByName(ctx context.Context, ownerID int64, name string) (*Repository, error) {
repo := new(Repository)
err := db.WithContext(ctx).
@@ -272,6 +301,66 @@ func (db *repos) GetByName(ctx context.Context, ownerID int64, name string) (*Re
return repo, nil
}
+func (db *repos) recountStars(tx *gorm.DB, userID, repoID int64) error {
+ /*
+ Equivalent SQL for PostgreSQL:
+
+ UPDATE repository
+ SET num_stars = (
+ SELECT COUNT(*) FROM star WHERE repo_id = @repoID
+ )
+ WHERE id = @repoID
+ */
+ err := tx.Model(&Repository{}).
+ Where("id = ?", repoID).
+ Update(
+ "num_stars",
+ tx.Model(&Star{}).Select("COUNT(*)").Where("repo_id = ?", repoID),
+ ).
+ Error
+ if err != nil {
+ return errors.Wrap(err, `update "repository.num_stars"`)
+ }
+
+ /*
+ Equivalent SQL for PostgreSQL:
+
+ UPDATE "user"
+ SET num_stars = (
+ SELECT COUNT(*) FROM star WHERE uid = @userID
+ )
+ WHERE id = @userID
+ */
+ err = tx.Model(&User{}).
+ Where("id = ?", userID).
+ Update(
+ "num_stars",
+ tx.Model(&Star{}).Select("COUNT(*)").Where("uid = ?", userID),
+ ).
+ Error
+ if err != nil {
+ return errors.Wrap(err, `update "user.num_stars"`)
+ }
+ return nil
+}
+
+func (db *repos) Star(ctx context.Context, userID, repoID int64) error {
+ return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ s := &Star{
+ UserID: userID,
+ RepoID: repoID,
+ }
+ result := tx.FirstOrCreate(s, s)
+ if result.Error != nil {
+ return errors.Wrap(result.Error, "upsert")
+ } else if result.RowsAffected <= 0 {
+ return nil // Relation already exists
+ }
+
+ return db.recountStars(tx, userID, repoID)
+ })
+}
+
func (db *repos) Touch(ctx context.Context, id int64) error {
return db.WithContext(ctx).
Model(new(Repository)).
diff --git a/internal/db/repos_test.go b/internal/db/repos_test.go
index 09289729..d6bfcb0d 100644
--- a/internal/db/repos_test.go
+++ b/internal/db/repos_test.go
@@ -85,7 +85,7 @@ func TestRepos(t *testing.T) {
}
t.Parallel()
- tables := []any{new(Repository), new(Access)}
+ tables := []any{new(Repository), new(Access), new(Watch), new(User), new(EmailAddress), new(Star)}
db := &repos{
DB: dbtest.NewDB(t, "repos", tables...),
}
@@ -97,7 +97,9 @@ func TestRepos(t *testing.T) {
{"Create", reposCreate},
{"GetByCollaboratorID", reposGetByCollaboratorID},
{"GetByCollaboratorIDWithAccessMode", reposGetByCollaboratorIDWithAccessMode},
+ {"GetByID", reposGetByID},
{"GetByName", reposGetByName},
+ {"Star", reposStar},
{"Touch", reposTouch},
} {
t.Run(tc.name, func(t *testing.T) {
@@ -154,6 +156,7 @@ func reposCreate(t *testing.T, db *repos) {
repo, err = db.GetByName(ctx, repo.OwnerID, repo.Name)
require.NoError(t, err)
assert.Equal(t, db.NowFunc().Format(time.RFC3339), repo.Created.UTC().Format(time.RFC3339))
+ assert.Equal(t, 1, repo.NumWatches) // The owner is watching the repo by default.
}
func reposGetByCollaboratorID(t *testing.T, db *repos) {
@@ -214,6 +217,21 @@ func reposGetByCollaboratorIDWithAccessMode(t *testing.T, db *repos) {
assert.Equal(t, AccessModeAdmin, accessModes[repo2.ID])
}
+func reposGetByID(t *testing.T, db *repos) {
+ ctx := context.Background()
+
+ repo1, err := db.Create(ctx, 1, CreateRepoOptions{Name: "repo1"})
+ require.NoError(t, err)
+
+ got, err := db.GetByID(ctx, repo1.ID)
+ require.NoError(t, err)
+ assert.Equal(t, repo1.Name, got.Name)
+
+ _, err = db.GetByID(ctx, 404)
+ wantErr := ErrRepoNotExist{args: errutil.Args{"repoID": int64(404)}}
+ assert.Equal(t, wantErr, err)
+}
+
func reposGetByName(t *testing.T, db *repos) {
ctx := context.Background()
@@ -232,6 +250,27 @@ func reposGetByName(t *testing.T, db *repos) {
assert.Equal(t, wantErr, err)
}
+func reposStar(t *testing.T, db *repos) {
+ ctx := context.Background()
+
+ repo1, err := db.Create(ctx, 1, CreateRepoOptions{Name: "repo1"})
+ require.NoError(t, err)
+ usersStore := NewUsersStore(db.DB)
+ alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{})
+ require.NoError(t, err)
+
+ err = db.Star(ctx, alice.ID, repo1.ID)
+ require.NoError(t, err)
+
+ repo1, err = db.GetByID(ctx, repo1.ID)
+ require.NoError(t, err)
+ assert.Equal(t, 1, repo1.NumStars)
+
+ alice, err = usersStore.GetByID(ctx, alice.ID)
+ require.NoError(t, err)
+ assert.Equal(t, 1, alice.NumStars)
+}
+
func reposTouch(t *testing.T, db *repos) {
ctx := context.Background()
diff --git a/internal/db/ssh_key.go b/internal/db/ssh_key.go
index 50ac324f..d022e207 100644
--- a/internal/db/ssh_key.go
+++ b/internal/db/ssh_key.go
@@ -517,6 +517,8 @@ func DeletePublicKey(doer *User, id int64) (err error) {
// RewriteAuthorizedKeys removes any authorized key and rewrite all keys from database again.
// Note: x.Iterate does not get latest data after insert/delete, so we have to call this function
// outside any session scope independently.
+//
+// Deprecated: Use PublicKeys.RewriteAuthorizedKeys instead.
func RewriteAuthorizedKeys() error {
sshOpLocker.Lock()
defer sshOpLocker.Unlock()
@@ -524,7 +526,7 @@ func RewriteAuthorizedKeys() error {
log.Trace("Doing: RewriteAuthorizedKeys")
_ = os.MkdirAll(conf.SSH.RootPath, os.ModePerm)
- fpath := filepath.Join(conf.SSH.RootPath, "authorized_keys")
+ fpath := authorizedKeysPath()
tmpPath := fpath + ".tmp"
f, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
diff --git a/internal/db/user.go b/internal/db/user.go
deleted file mode 100644
index 9cbc2fa3..00000000
--- a/internal/db/user.go
+++ /dev/null
@@ -1,197 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package db
-
-import (
- "fmt"
- _ "image/jpeg"
- "os"
- "time"
-
- "xorm.io/xorm"
-
- "gogs.io/gogs/internal/repoutil"
- "gogs.io/gogs/internal/userutil"
-)
-
-// TODO(unknwon): Delete me once refactoring is done.
-func (u *User) BeforeInsert() {
- u.CreatedUnix = time.Now().Unix()
- u.UpdatedUnix = u.CreatedUnix
-}
-
-// TODO(unknwon): Delete me once refactoring is done.
-func (u *User) AfterSet(colName string, _ xorm.Cell) {
- switch colName {
- case "created_unix":
- u.Created = time.Unix(u.CreatedUnix, 0).Local()
- case "updated_unix":
- u.Updated = time.Unix(u.UpdatedUnix, 0).Local()
- }
-}
-
-// deleteBeans deletes all given beans, beans should contain delete conditions.
-func deleteBeans(e Engine, beans ...any) (err error) {
- for i := range beans {
- if _, err = e.Delete(beans[i]); err != nil {
- return err
- }
- }
- return nil
-}
-
-// FIXME: need some kind of mechanism to record failure. HINT: system notice
-func deleteUser(e *xorm.Session, u *User) error {
- // Note: A user owns any repository or belongs to any organization
- // cannot perform delete operation.
-
- // Check ownership of repository.
- count, err := getRepositoryCount(e, u)
- if err != nil {
- return fmt.Errorf("GetRepositoryCount: %v", err)
- } else if count > 0 {
- return ErrUserOwnRepos{UID: u.ID}
- }
-
- // Check membership of organization.
- count, err = u.getOrganizationCount(e)
- if err != nil {
- return fmt.Errorf("GetOrganizationCount: %v", err)
- } else if count > 0 {
- return ErrUserHasOrgs{UID: u.ID}
- }
-
- // ***** START: Watch *****
- watches := make([]*Watch, 0, 10)
- if err = e.Find(&watches, &Watch{UserID: u.ID}); err != nil {
- return fmt.Errorf("get all watches: %v", err)
- }
- for i := range watches {
- if _, err = e.Exec("UPDATE `repository` SET num_watches=num_watches-1 WHERE id=?", watches[i].RepoID); err != nil {
- return fmt.Errorf("decrease repository watch number[%d]: %v", watches[i].RepoID, err)
- }
- }
- // ***** END: Watch *****
-
- // ***** START: Star *****
- stars := make([]*Star, 0, 10)
- if err = e.Find(&stars, &Star{UID: u.ID}); err != nil {
- return fmt.Errorf("get all stars: %v", err)
- }
- for i := range stars {
- if _, err = e.Exec("UPDATE `repository` SET num_stars=num_stars-1 WHERE id=?", stars[i].RepoID); err != nil {
- return fmt.Errorf("decrease repository star number[%d]: %v", stars[i].RepoID, err)
- }
- }
- // ***** END: Star *****
-
- // ***** START: Follow *****
- followers := make([]*Follow, 0, 10)
- if err = e.Find(&followers, &Follow{UserID: u.ID}); err != nil {
- return fmt.Errorf("get all followers: %v", err)
- }
- for i := range followers {
- if _, err = e.Exec("UPDATE `user` SET num_followers=num_followers-1 WHERE id=?", followers[i].UserID); err != nil {
- return fmt.Errorf("decrease user follower number[%d]: %v", followers[i].UserID, err)
- }
- }
- // ***** END: Follow *****
-
- if err = deleteBeans(e,
- &AccessToken{UserID: u.ID},
- &Collaboration{UserID: u.ID},
- &Access{UserID: u.ID},
- &Watch{UserID: u.ID},
- &Star{UID: u.ID},
- &Follow{FollowID: u.ID},
- &Action{UserID: u.ID},
- &IssueUser{UID: u.ID},
- &EmailAddress{UserID: u.ID},
- ); err != nil {
- return fmt.Errorf("deleteBeans: %v", err)
- }
-
- // ***** START: PublicKey *****
- keys := make([]*PublicKey, 0, 10)
- if err = e.Find(&keys, &PublicKey{OwnerID: u.ID}); err != nil {
- return fmt.Errorf("get all public keys: %v", err)
- }
-
- keyIDs := make([]int64, len(keys))
- for i := range keys {
- keyIDs[i] = keys[i].ID
- }
- if err = deletePublicKeys(e, keyIDs...); err != nil {
- return fmt.Errorf("deletePublicKeys: %v", err)
- }
- // ***** END: PublicKey *****
-
- // Clear assignee.
- if _, err = e.Exec("UPDATE `issue` SET assignee_id=0 WHERE assignee_id=?", u.ID); err != nil {
- return fmt.Errorf("clear assignee: %v", err)
- }
-
- if _, err = e.ID(u.ID).Delete(new(User)); err != nil {
- return fmt.Errorf("Delete: %v", err)
- }
-
- // FIXME: system notice
- // Note: There are something just cannot be roll back,
- // so just keep error logs of those operations.
-
- _ = os.RemoveAll(repoutil.UserPath(u.Name))
- _ = os.Remove(userutil.CustomAvatarPath(u.ID))
-
- return nil
-}
-
-// Deprecated: Use OrgsUsers.CountByUser instead.
-//
-// TODO(unknwon): Delete me once no more call sites in this file.
-func (u *User) getOrganizationCount(e Engine) (int64, error) {
- return e.Where("uid=?", u.ID).Count(new(OrgUser))
-}
-
-// DeleteUser completely and permanently deletes everything of a user,
-// but issues/comments/pulls will be kept and shown as someone has been deleted.
-func DeleteUser(u *User) (err error) {
- sess := x.NewSession()
- defer sess.Close()
- if err = sess.Begin(); err != nil {
- return err
- }
-
- if err = deleteUser(sess, u); err != nil {
- // Note: don't wrapper error here.
- return err
- }
-
- if err = sess.Commit(); err != nil {
- return err
- }
-
- return RewriteAuthorizedKeys()
-}
-
-// DeleteInactivateUsers deletes all inactivate users and email addresses.
-func DeleteInactivateUsers() (err error) {
- users := make([]*User, 0, 10)
- if err = x.Where("is_active = ?", false).Find(&users); err != nil {
- return fmt.Errorf("get all inactive users: %v", err)
- }
- // FIXME: should only update authorized_keys file once after all deletions.
- for _, u := range users {
- if err = DeleteUser(u); err != nil {
- // Ignore users that were set inactive by admin.
- if IsErrUserOwnRepos(err) || IsErrUserHasOrgs(err) {
- continue
- }
- return err
- }
- }
-
- _, err = x.Where("is_activated = ?", false).Delete(new(EmailAddress))
- return err
-}
diff --git a/internal/db/users.go b/internal/db/users.go
index 7b2b550e..51810dc7 100644
--- a/internal/db/users.go
+++ b/internal/db/users.go
@@ -6,6 +6,7 @@ package db
import (
"context"
+ "database/sql"
"fmt"
"os"
"strings"
@@ -61,6 +62,14 @@ type UsersStore interface {
// DeleteCustomAvatar deletes the current user custom avatar and falls back to
// use look up avatar by email.
DeleteCustomAvatar(ctx context.Context, userID int64) error
+ // DeleteByID deletes the given user and all their resources. It returns
+ // ErrUserOwnRepos when the user still has repository ownership, or returns
+ // ErrUserHasOrgs when the user still has organization membership. It is more
+ // performant to skip rewriting the "authorized_keys" file for individual
+ // deletion in a batch operation.
+ DeleteByID(ctx context.Context, userID int64, skipRewriteAuthorizedKeys bool) error
+ // DeleteInactivated deletes all inactivated users.
+ DeleteInactivated() error
// GetByEmail returns the user (not organization) with given email. It ignores
// records with unverified emails and returns ErrUserNotExist when not found.
GetByEmail(ctx context.Context, email string) (*User, error)
@@ -423,6 +432,224 @@ func (db *users) DeleteCustomAvatar(ctx context.Context, userID int64) error {
Error
}
+type ErrUserOwnRepos struct {
+ args errutil.Args
+}
+
+// IsErrUserOwnRepos returns true if the underlying error has the type
+// ErrUserOwnRepos.
+func IsErrUserOwnRepos(err error) bool {
+ _, ok := errors.Cause(err).(ErrUserOwnRepos)
+ return ok
+}
+
+func (err ErrUserOwnRepos) Error() string {
+ return fmt.Sprintf("user still has repository ownership: %v", err.args)
+}
+
+type ErrUserHasOrgs struct {
+ args errutil.Args
+}
+
+// IsErrUserHasOrgs returns true if the underlying error has the type
+// ErrUserHasOrgs.
+func IsErrUserHasOrgs(err error) bool {
+ _, ok := errors.Cause(err).(ErrUserHasOrgs)
+ return ok
+}
+
+func (err ErrUserHasOrgs) Error() string {
+ return fmt.Sprintf("user still has organization membership: %v", err.args)
+}
+
+func (db *users) DeleteByID(ctx context.Context, userID int64, skipRewriteAuthorizedKeys bool) error {
+ user, err := db.GetByID(ctx, userID)
+ if err != nil {
+ if IsErrUserNotExist(err) {
+ return nil
+ }
+ return errors.Wrap(err, "get user")
+ }
+
+ // Double check the user is not a direct owner of any repository and not a
+ // member of any organization.
+ var count int64
+ err = db.WithContext(ctx).Model(&Repository{}).Where("owner_id = ?", userID).Count(&count).Error
+ if err != nil {
+ return errors.Wrap(err, "count repositories")
+ } else if count > 0 {
+ return ErrUserOwnRepos{args: errutil.Args{"userID": userID}}
+ }
+
+ err = db.WithContext(ctx).Model(&OrgUser{}).Where("uid = ?", userID).Count(&count).Error
+ if err != nil {
+ return errors.Wrap(err, "count organization membership")
+ } else if count > 0 {
+ return ErrUserHasOrgs{args: errutil.Args{"userID": userID}}
+ }
+
+ needsRewriteAuthorizedKeys := false
+ err = db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ /*
+ Equivalent SQL for PostgreSQL:
+
+ UPDATE repository
+ SET num_watches = num_watches - 1
+ WHERE id IN (
+ SELECT repo_id FROM watch WHERE user_id = @userID
+ )
+ */
+ err = tx.Table("repository").
+ Where("id IN (?)", tx.
+ Select("repo_id").
+ Table("watch").
+ Where("user_id = ?", userID),
+ ).
+ UpdateColumn("num_watches", gorm.Expr("num_watches - 1")).
+ Error
+ if err != nil {
+ return errors.Wrap(err, `decrease "repository.num_watches"`)
+ }
+
+ /*
+ Equivalent SQL for PostgreSQL:
+
+ UPDATE repository
+ SET num_stars = num_stars - 1
+ WHERE id IN (
+ SELECT repo_id FROM star WHERE uid = @userID
+ )
+ */
+ err = tx.Table("repository").
+ Where("id IN (?)", tx.
+ Select("repo_id").
+ Table("star").
+ Where("uid = ?", userID),
+ ).
+ UpdateColumn("num_stars", gorm.Expr("num_stars - 1")).
+ Error
+ if err != nil {
+ return errors.Wrap(err, `decrease "repository.num_stars"`)
+ }
+
+ /*
+ Equivalent SQL for PostgreSQL:
+
+ UPDATE user
+ SET num_followers = num_followers - 1
+ WHERE id IN (
+ SELECT follow_id FROM follow WHERE user_id = @userID
+ )
+ */
+ err = tx.Table("user").
+ Where("id IN (?)", tx.
+ Select("follow_id").
+ Table("follow").
+ Where("user_id = ?", userID),
+ ).
+ UpdateColumn("num_followers", gorm.Expr("num_followers - 1")).
+ Error
+ if err != nil {
+ return errors.Wrap(err, `decrease "user.num_followers"`)
+ }
+
+ /*
+ Equivalent SQL for PostgreSQL:
+
+ UPDATE user
+ SET num_following = num_following - 1
+ WHERE id IN (
+ SELECT user_id FROM follow WHERE follow_id = @userID
+ )
+ */
+ err = tx.Table("user").
+ Where("id IN (?)", tx.
+ Select("user_id").
+ Table("follow").
+ Where("follow_id = ?", userID),
+ ).
+ UpdateColumn("num_following", gorm.Expr("num_following - 1")).
+ Error
+ if err != nil {
+ return errors.Wrap(err, `decrease "user.num_following"`)
+ }
+
+ if !skipRewriteAuthorizedKeys {
+ // We need to rewrite "authorized_keys" file if the user owns any public keys.
+ needsRewriteAuthorizedKeys = tx.Where("owner_id = ?", userID).First(&PublicKey{}).Error != gorm.ErrRecordNotFound
+ }
+
+ err = tx.Model(&Issue{}).Where("assignee_id = ?", userID).Update("assignee_id", 0).Error
+ if err != nil {
+ return errors.Wrap(err, "clear assignees")
+ }
+
+ for _, t := range []struct {
+ table any
+ where string
+ }{
+ {&Watch{}, "user_id = @userID"},
+ {&Star{}, "uid = @userID"},
+ {&Follow{}, "user_id = @userID OR follow_id = @userID"},
+ {&PublicKey{}, "owner_id = @userID"},
+
+ {&AccessToken{}, "uid = @userID"},
+ {&Collaboration{}, "user_id = @userID"},
+ {&Access{}, "user_id = @userID"},
+ {&Action{}, "user_id = @userID"},
+ {&IssueUser{}, "uid = @userID"},
+ {&EmailAddress{}, "uid = @userID"},
+ {&User{}, "id = @userID"},
+ } {
+ err = tx.Where(t.where, sql.Named("userID", userID)).Delete(t.table).Error
+ if err != nil {
+ return errors.Wrapf(err, "clean up table %T", t.table)
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ _ = os.RemoveAll(repoutil.UserPath(user.Name))
+ _ = os.Remove(userutil.CustomAvatarPath(userID))
+
+ if needsRewriteAuthorizedKeys {
+ err = NewPublicKeysStore(db.DB).RewriteAuthorizedKeys()
+ if err != nil {
+ return errors.Wrap(err, `rewrite "authorized_keys" file`)
+ }
+ }
+ return nil
+}
+
+// NOTE: We do not take context.Context here because this operation in practice
+// could much longer than the general request timeout (e.g. one minute).
+func (db *users) DeleteInactivated() error {
+ var userIDs []int64
+ err := db.Model(&User{}).Where("is_active = ?", false).Pluck("id", &userIDs).Error
+ if err != nil {
+ return errors.Wrap(err, "get inactivated user IDs")
+ }
+
+ for _, userID := range userIDs {
+ err = db.DeleteByID(context.Background(), userID, true)
+ if err != nil {
+ // Skip users that may had set to inactivated by admins.
+ if IsErrUserOwnRepos(err) || IsErrUserHasOrgs(err) {
+ continue
+ }
+ return errors.Wrapf(err, "delete user with ID %d", userID)
+ }
+ }
+ err = NewPublicKeysStore(db.DB).RewriteAuthorizedKeys()
+ if err != nil {
+ return errors.Wrap(err, `rewrite "authorized_keys" file`)
+ }
+ return nil
+}
+
var _ errutil.NotFound = (*ErrUserNotExist)(nil)
type ErrUserNotExist struct {
diff --git a/internal/db/users_test.go b/internal/db/users_test.go
index 171b3a88..69f157ea 100644
--- a/internal/db/users_test.go
+++ b/internal/db/users_test.go
@@ -82,7 +82,11 @@ func TestUsers(t *testing.T) {
}
t.Parallel()
- tables := []any{new(User), new(EmailAddress), new(Repository), new(Follow), new(PullRequest), new(PublicKey)}
+ tables := []any{
+ new(User), new(EmailAddress), new(Repository), new(Follow), new(PullRequest), new(PublicKey), new(OrgUser),
+ new(Watch), new(Star), new(Issue), new(AccessToken), new(Collaboration), new(Action), new(IssueUser),
+ new(Access),
+ }
db := &users{
DB: dbtest.NewDB(t, "users", tables...),
}
@@ -96,6 +100,8 @@ func TestUsers(t *testing.T) {
{"Count", usersCount},
{"Create", usersCreate},
{"DeleteCustomAvatar", usersDeleteCustomAvatar},
+ {"DeleteByID", usersDeleteByID},
+ {"DeleteInactivated", usersDeleteInactivated},
{"GetByEmail", usersGetByEmail},
{"GetByID", usersGetByID},
{"GetByUsername", usersGetByUsername},
@@ -463,6 +469,266 @@ func usersDeleteCustomAvatar(t *testing.T, db *users) {
assert.False(t, alice.UseCustomAvatar)
}
+func usersDeleteByID(t *testing.T, db *users) {
+ ctx := context.Background()
+ reposStore := NewReposStore(db.DB)
+
+ t.Run("user still has repository ownership", func(t *testing.T) {
+ alice, err := db.Create(ctx, "alice", "alice@exmaple.com", CreateUserOptions{})
+ require.NoError(t, err)
+
+ _, err = reposStore.Create(ctx, alice.ID, CreateRepoOptions{Name: "repo1"})
+ require.NoError(t, err)
+
+ err = db.DeleteByID(ctx, alice.ID, false)
+ wantErr := ErrUserOwnRepos{errutil.Args{"userID": alice.ID}}
+ assert.Equal(t, wantErr, err)
+ })
+
+ t.Run("user still has organization membership", func(t *testing.T) {
+ bob, err := db.Create(ctx, "bob", "bob@exmaple.com", CreateUserOptions{})
+ require.NoError(t, err)
+
+ // TODO: Use Orgs.Create to replace SQL hack when the method is available.
+ org1, err := db.Create(ctx, "org1", "org1@example.com", CreateUserOptions{})
+ require.NoError(t, err)
+ err = db.Exec(
+ dbutil.Quote("UPDATE %s SET type = ? WHERE id IN (?)", "user"),
+ UserTypeOrganization, org1.ID,
+ ).Error
+ require.NoError(t, err)
+
+ // TODO: Use Orgs.Join to replace SQL hack when the method is available.
+ err = db.Exec(`INSERT INTO org_user (uid, org_id) VALUES (?, ?)`, bob.ID, org1.ID).Error
+ require.NoError(t, err)
+
+ err = db.DeleteByID(ctx, bob.ID, false)
+ wantErr := ErrUserHasOrgs{errutil.Args{"userID": bob.ID}}
+ assert.Equal(t, wantErr, err)
+ })
+
+ cindy, err := db.Create(ctx, "cindy", "cindy@exmaple.com", CreateUserOptions{})
+ require.NoError(t, err)
+ frank, err := db.Create(ctx, "frank", "frank@exmaple.com", CreateUserOptions{})
+ require.NoError(t, err)
+ repo2, err := reposStore.Create(ctx, cindy.ID, CreateRepoOptions{Name: "repo2"})
+ require.NoError(t, err)
+
+ testUser, err := db.Create(ctx, "testUser", "testUser@exmaple.com", CreateUserOptions{})
+ require.NoError(t, err)
+
+ // Mock watches, stars and follows
+ err = NewWatchesStore(db.DB).Watch(ctx, testUser.ID, repo2.ID)
+ require.NoError(t, err)
+ err = reposStore.Star(ctx, testUser.ID, repo2.ID)
+ require.NoError(t, err)
+ followsStore := NewFollowsStore(db.DB)
+ err = followsStore.Follow(ctx, testUser.ID, cindy.ID)
+ require.NoError(t, err)
+ err = followsStore.Follow(ctx, frank.ID, testUser.ID)
+ require.NoError(t, err)
+
+ // Mock "authorized_keys" file
+ // TODO: Use PublicKeys.Add to replace SQL hack when the method is available.
+ publicKey := &PublicKey{
+ OwnerID: testUser.ID,
+ Name: "test-key",
+ Fingerprint: "12:f8:7e:78:61:b4:bf:e2:de:24:15:96:4e:d4:72:53",
+ Content: "test-key-content",
+ }
+ err = db.DB.Create(publicKey).Error
+ require.NoError(t, err)
+ tempSSHRootPath := filepath.Join(os.TempDir(), "usersDeleteByID-tempSSHRootPath")
+ conf.SetMockSSH(t, conf.SSHOpts{RootPath: tempSSHRootPath})
+ err = NewPublicKeysStore(db.DB).RewriteAuthorizedKeys()
+ require.NoError(t, err)
+
+ // Mock issue assignee
+ // TODO: Use Issues.Assign to replace SQL hack when the method is available.
+ issue := &Issue{
+ RepoID: repo2.ID,
+ Index: 1,
+ PosterID: cindy.ID,
+ Title: "test-issue",
+ AssigneeID: testUser.ID,
+ }
+ err = db.DB.Create(issue).Error
+ require.NoError(t, err)
+
+ // Mock random entries in related tables
+ for _, table := range []any{
+ &AccessToken{UserID: testUser.ID},
+ &Collaboration{UserID: testUser.ID},
+ &Access{UserID: testUser.ID},
+ &Action{UserID: testUser.ID},
+ &IssueUser{UserID: testUser.ID},
+ &EmailAddress{UserID: testUser.ID},
+ } {
+ err = db.DB.Create(table).Error
+ require.NoError(t, err, "table for %T", table)
+ }
+
+ // Mock user directory
+ tempRepositoryRoot := filepath.Join(os.TempDir(), "usersDeleteByID-tempRepositoryRoot")
+ conf.SetMockRepository(t, conf.RepositoryOpts{Root: tempRepositoryRoot})
+ tempUserPath := repoutil.UserPath(testUser.Name)
+ err = os.MkdirAll(tempUserPath, os.ModePerm)
+ require.NoError(t, err)
+
+ // Mock user custom avatar
+ tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "usersDeleteByID-tempPictureAvatarUploadPath")
+ conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath})
+ err = os.MkdirAll(tempPictureAvatarUploadPath, os.ModePerm)
+ require.NoError(t, err)
+ tempCustomAvatarPath := userutil.CustomAvatarPath(testUser.ID)
+ err = os.WriteFile(tempCustomAvatarPath, []byte("test"), 0600)
+ require.NoError(t, err)
+
+ // Verify mock data
+ repo2, err = reposStore.GetByID(ctx, repo2.ID)
+ require.NoError(t, err)
+ assert.Equal(t, 2, repo2.NumWatches) // The owner is watching the repo by default.
+ assert.Equal(t, 1, repo2.NumStars)
+
+ cindy, err = db.GetByID(ctx, cindy.ID)
+ require.NoError(t, err)
+ assert.Equal(t, 1, cindy.NumFollowers)
+ frank, err = db.GetByID(ctx, frank.ID)
+ require.NoError(t, err)
+ assert.Equal(t, 1, frank.NumFollowing)
+
+ authorizedKeys, err := os.ReadFile(authorizedKeysPath())
+ require.NoError(t, err)
+ assert.Contains(t, string(authorizedKeys), fmt.Sprintf("key-%d", publicKey.ID))
+ assert.Contains(t, string(authorizedKeys), publicKey.Content)
+
+ // TODO: Use Issues.GetByID to replace SQL hack when the method is available.
+ err = db.DB.First(issue, issue.ID).Error
+ require.NoError(t, err)
+ assert.Equal(t, testUser.ID, issue.AssigneeID)
+
+ relatedTables := []any{
+ &Watch{UserID: testUser.ID},
+ &Star{UserID: testUser.ID},
+ &Follow{UserID: testUser.ID},
+ &PublicKey{OwnerID: testUser.ID},
+ &AccessToken{UserID: testUser.ID},
+ &Collaboration{UserID: testUser.ID},
+ &Access{UserID: testUser.ID},
+ &Action{UserID: testUser.ID},
+ &IssueUser{UserID: testUser.ID},
+ &EmailAddress{UserID: testUser.ID},
+ }
+ for _, table := range relatedTables {
+ var count int64
+ err = db.DB.Model(table).Where(table).Count(&count).Error
+ require.NoError(t, err, "table for %T", table)
+ assert.NotZero(t, count, "table for %T", table)
+ }
+
+ assert.True(t, osutil.IsExist(tempUserPath))
+ assert.True(t, osutil.IsExist(tempCustomAvatarPath))
+
+ // Pull the trigger
+ err = db.DeleteByID(ctx, testUser.ID, false)
+ require.NoError(t, err)
+
+ // Verify after-the-fact data
+ repo2, err = reposStore.GetByID(ctx, repo2.ID)
+ require.NoError(t, err)
+ assert.Equal(t, 1, repo2.NumWatches) // The owner is watching the repo by default.
+ assert.Equal(t, 0, repo2.NumStars)
+
+ cindy, err = db.GetByID(ctx, cindy.ID)
+ require.NoError(t, err)
+ assert.Equal(t, 0, cindy.NumFollowers)
+ frank, err = db.GetByID(ctx, frank.ID)
+ require.NoError(t, err)
+ assert.Equal(t, 0, frank.NumFollowing)
+
+ authorizedKeys, err = os.ReadFile(authorizedKeysPath())
+ require.NoError(t, err)
+ assert.Empty(t, authorizedKeys)
+
+ // TODO: Use Issues.GetByID to replace SQL hack when the method is available.
+ err = db.DB.First(issue, issue.ID).Error
+ require.NoError(t, err)
+ assert.Equal(t, int64(0), issue.AssigneeID)
+
+ for _, table := range []any{
+ &Watch{UserID: testUser.ID},
+ &Star{UserID: testUser.ID},
+ &Follow{UserID: testUser.ID},
+ &PublicKey{OwnerID: testUser.ID},
+ &AccessToken{UserID: testUser.ID},
+ &Collaboration{UserID: testUser.ID},
+ &Access{UserID: testUser.ID},
+ &Action{UserID: testUser.ID},
+ &IssueUser{UserID: testUser.ID},
+ &EmailAddress{UserID: testUser.ID},
+ } {
+ var count int64
+ err = db.DB.Model(table).Where(table).Count(&count).Error
+ require.NoError(t, err, "table for %T", table)
+ assert.Equal(t, int64(0), count, "table for %T", table)
+ }
+
+ assert.False(t, osutil.IsExist(tempUserPath))
+ assert.False(t, osutil.IsExist(tempCustomAvatarPath))
+
+ _, err = db.GetByID(ctx, testUser.ID)
+ wantErr := ErrUserNotExist{errutil.Args{"userID": testUser.ID}}
+ assert.Equal(t, wantErr, err)
+}
+
+func usersDeleteInactivated(t *testing.T, db *users) {
+ ctx := context.Background()
+
+ // User with repository ownership should be skipped
+ alice, err := db.Create(ctx, "alice", "alice@exmaple.com", CreateUserOptions{})
+ require.NoError(t, err)
+ reposStore := NewReposStore(db.DB)
+ _, err = reposStore.Create(ctx, alice.ID, CreateRepoOptions{Name: "repo1"})
+ require.NoError(t, err)
+
+ // User with organization membership should be skipped
+ bob, err := db.Create(ctx, "bob", "bob@exmaple.com", CreateUserOptions{})
+ require.NoError(t, err)
+ // TODO: Use Orgs.Create to replace SQL hack when the method is available.
+ org1, err := db.Create(ctx, "org1", "org1@example.com", CreateUserOptions{})
+ require.NoError(t, err)
+ err = db.Exec(
+ dbutil.Quote("UPDATE %s SET type = ? WHERE id IN (?)", "user"),
+ UserTypeOrganization, org1.ID,
+ ).Error
+ require.NoError(t, err)
+ // TODO: Use Orgs.Join to replace SQL hack when the method is available.
+ err = db.Exec(`INSERT INTO org_user (uid, org_id) VALUES (?, ?)`, bob.ID, org1.ID).Error
+ require.NoError(t, err)
+
+ // User activated state should be skipped
+ _, err = db.Create(ctx, "cindy", "cindy@exmaple.com", CreateUserOptions{Activated: true})
+ require.NoError(t, err)
+
+ // User meant to be deleted
+ david, err := db.Create(ctx, "david", "david@exmaple.com", CreateUserOptions{})
+ require.NoError(t, err)
+
+ tempSSHRootPath := filepath.Join(os.TempDir(), "usersDeleteInactivated-tempSSHRootPath")
+ conf.SetMockSSH(t, conf.SSHOpts{RootPath: tempSSHRootPath})
+
+ err = db.DeleteInactivated()
+ require.NoError(t, err)
+
+ _, err = db.GetByID(ctx, david.ID)
+ wantErr := ErrUserNotExist{errutil.Args{"userID": david.ID}}
+ assert.Equal(t, wantErr, err)
+
+ users, err := db.List(ctx, 1, 10)
+ require.NoError(t, err)
+ require.Len(t, users, 3)
+}
+
func usersGetByEmail(t *testing.T, db *users) {
ctx := context.Background()
diff --git a/internal/db/watches.go b/internal/db/watches.go
index e93a4ab6..6ca78a37 100644
--- a/internal/db/watches.go
+++ b/internal/db/watches.go
@@ -7,6 +7,7 @@ package db
import (
"context"
+ "github.com/pkg/errors"
"gorm.io/gorm"
)
@@ -16,6 +17,8 @@ import (
type WatchesStore interface {
// ListByRepo returns all watches of the given repository.
ListByRepo(ctx context.Context, repoID int64) ([]*Watch, error)
+ // Watch marks the user to watch the repository.
+ Watch(ctx context.Context, userID, repoID int64) error
}
var Watches WatchesStore
@@ -36,3 +39,39 @@ func (db *watches) ListByRepo(ctx context.Context, repoID int64) ([]*Watch, erro
var watches []*Watch
return watches, db.WithContext(ctx).Where("repo_id = ?", repoID).Find(&watches).Error
}
+
+func (db *watches) updateWatchingCount(tx *gorm.DB, repoID int64) error {
+ /*
+ Equivalent SQL for PostgreSQL:
+
+ UPDATE repository
+ SET num_watches = (
+ SELECT COUNT(*) FROM watch WHERE repo_id = @repoID
+ )
+ WHERE id = @repoID
+ */
+ return tx.Model(&Repository{}).
+ Where("id = ?", repoID).
+ Update(
+ "num_watches",
+ tx.Model(&Watch{}).Select("COUNT(*)").Where("repo_id = ?", repoID),
+ ).
+ Error
+}
+
+func (db *watches) Watch(ctx context.Context, userID, repoID int64) error {
+ return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ w := &Watch{
+ UserID: userID,
+ RepoID: repoID,
+ }
+ result := tx.FirstOrCreate(w, w)
+ if result.Error != nil {
+ return errors.Wrap(result.Error, "upsert")
+ } else if result.RowsAffected <= 0 {
+ return nil // Relation already exists
+ }
+
+ return db.updateWatchingCount(tx, repoID)
+ })
+}
diff --git a/internal/db/watches_test.go b/internal/db/watches_test.go
index 973f64d4..245be7b3 100644
--- a/internal/db/watches_test.go
+++ b/internal/db/watches_test.go
@@ -5,8 +5,10 @@
package db
import (
+ "context"
"testing"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gogs.io/gogs/internal/dbtest"
@@ -18,7 +20,7 @@ func TestWatches(t *testing.T) {
}
t.Parallel()
- tables := []any{new(Watch)}
+ tables := []any{new(Watch), new(Repository)}
db := &watches{
DB: dbtest.NewDB(t, "watches", tables...),
}
@@ -28,6 +30,7 @@ func TestWatches(t *testing.T) {
test func(t *testing.T, db *watches)
}{
{"ListByRepo", watchesListByRepo},
+ {"Watch", watchesWatch},
} {
t.Run(tc.name, func(t *testing.T) {
t.Cleanup(func() {
@@ -42,6 +45,44 @@ func TestWatches(t *testing.T) {
}
}
-func watchesListByRepo(_ *testing.T, _ *watches) {
- // TODO: Add tests once WatchRepo is migrated to GORM.
+func watchesListByRepo(t *testing.T, db *watches) {
+ ctx := context.Background()
+
+ err := db.Watch(ctx, 1, 1)
+ require.NoError(t, err)
+ err = db.Watch(ctx, 2, 1)
+ require.NoError(t, err)
+ err = db.Watch(ctx, 2, 2)
+ require.NoError(t, err)
+
+ got, err := db.ListByRepo(ctx, 1)
+ require.NoError(t, err)
+ for _, w := range got {
+ w.ID = 0
+ }
+
+ want := []*Watch{
+ {UserID: 1, RepoID: 1},
+ {UserID: 2, RepoID: 1},
+ }
+ assert.Equal(t, want, got)
+}
+
+func watchesWatch(t *testing.T, db *watches) {
+ ctx := context.Background()
+
+ reposStore := NewReposStore(db.DB)
+ repo1, err := reposStore.Create(ctx, 1, CreateRepoOptions{Name: "repo1"})
+ require.NoError(t, err)
+
+ err = db.Watch(ctx, 2, repo1.ID)
+ require.NoError(t, err)
+
+ // It is OK to watch multiple times and just be noop.
+ err = db.Watch(ctx, 2, repo1.ID)
+ require.NoError(t, err)
+
+ repo1, err = reposStore.GetByID(ctx, repo1.ID)
+ require.NoError(t, err)
+ assert.Equal(t, 2, repo1.NumWatches) // The owner is watching the repo by default.
}
diff --git a/internal/route/admin/admin.go b/internal/route/admin/admin.go
index 8fde8da8..f585163c 100644
--- a/internal/route/admin/admin.go
+++ b/internal/route/admin/admin.go
@@ -145,7 +145,7 @@ func Operation(c *context.Context) {
switch AdminOperation(c.QueryInt("op")) {
case CleanInactivateUser:
success = c.Tr("admin.dashboard.delete_inactivate_accounts_success")
- err = db.DeleteInactivateUsers()
+ err = db.Users.DeleteInactivated()
case CleanRepoArchives:
success = c.Tr("admin.dashboard.delete_repo_archives_success")
err = db.DeleteRepositoryArchives()
diff --git a/internal/route/admin/users.go b/internal/route/admin/users.go
index 1b49d216..b00d447b 100644
--- a/internal/route/admin/users.go
+++ b/internal/route/admin/users.go
@@ -226,7 +226,7 @@ func DeleteUser(c *context.Context) {
return
}
- if err = db.DeleteUser(u); err != nil {
+ if err = db.Users.DeleteByID(c.Req.Context(), u.ID, false); err != nil {
switch {
case db.IsErrUserOwnRepos(err):
c.Flash.Error(c.Tr("admin.users.still_own_repo"))
diff --git a/internal/route/api/v1/admin/user.go b/internal/route/api/v1/admin/user.go
index 97f7a0bb..cf511902 100644
--- a/internal/route/api/v1/admin/user.go
+++ b/internal/route/api/v1/admin/user.go
@@ -129,7 +129,7 @@ func DeleteUser(c *context.APIContext) {
return
}
- if err := db.DeleteUser(u); err != nil {
+ if err := db.Users.DeleteByID(c.Req.Context(), u.ID, false); err != nil {
if db.IsErrUserOwnRepos(err) ||
db.IsErrUserHasOrgs(err) {
c.ErrorStatus(http.StatusUnprocessableEntity, err)
diff --git a/internal/route/lfs/mocks_test.go b/internal/route/lfs/mocks_test.go
index 089004fc..ad71f6cf 100644
--- a/internal/route/lfs/mocks_test.go
+++ b/internal/route/lfs/mocks_test.go
@@ -1502,9 +1502,15 @@ type MockReposStore struct {
// function object controlling the behavior of the method
// GetByCollaboratorIDWithAccessMode.
GetByCollaboratorIDWithAccessModeFunc *ReposStoreGetByCollaboratorIDWithAccessModeFunc
+ // GetByIDFunc is an instance of a mock function object controlling the
+ // behavior of the method GetByID.
+ GetByIDFunc *ReposStoreGetByIDFunc
// GetByNameFunc is an instance of a mock function object controlling
// the behavior of the method GetByName.
GetByNameFunc *ReposStoreGetByNameFunc
+ // StarFunc is an instance of a mock function object controlling the
+ // behavior of the method Star.
+ StarFunc *ReposStoreStarFunc
// TouchFunc is an instance of a mock function object controlling the
// behavior of the method Touch.
TouchFunc *ReposStoreTouchFunc
@@ -1529,11 +1535,21 @@ func NewMockReposStore() *MockReposStore {
return
},
},
+ GetByIDFunc: &ReposStoreGetByIDFunc{
+ defaultHook: func(context.Context, int64) (r0 *db.Repository, r1 error) {
+ return
+ },
+ },
GetByNameFunc: &ReposStoreGetByNameFunc{
defaultHook: func(context.Context, int64, string) (r0 *db.Repository, r1 error) {
return
},
},
+ StarFunc: &ReposStoreStarFunc{
+ defaultHook: func(context.Context, int64, int64) (r0 error) {
+ return
+ },
+ },
TouchFunc: &ReposStoreTouchFunc{
defaultHook: func(context.Context, int64) (r0 error) {
return
@@ -1561,11 +1577,21 @@ func NewStrictMockReposStore() *MockReposStore {
panic("unexpected invocation of MockReposStore.GetByCollaboratorIDWithAccessMode")
},
},
+ GetByIDFunc: &ReposStoreGetByIDFunc{
+ defaultHook: func(context.Context, int64) (*db.Repository, error) {
+ panic("unexpected invocation of MockReposStore.GetByID")
+ },
+ },
GetByNameFunc: &ReposStoreGetByNameFunc{
defaultHook: func(context.Context, int64, string) (*db.Repository, error) {
panic("unexpected invocation of MockReposStore.GetByName")
},
},
+ StarFunc: &ReposStoreStarFunc{
+ defaultHook: func(context.Context, int64, int64) error {
+ panic("unexpected invocation of MockReposStore.Star")
+ },
+ },
TouchFunc: &ReposStoreTouchFunc{
defaultHook: func(context.Context, int64) error {
panic("unexpected invocation of MockReposStore.Touch")
@@ -1587,9 +1613,15 @@ func NewMockReposStoreFrom(i db.ReposStore) *MockReposStore {
GetByCollaboratorIDWithAccessModeFunc: &ReposStoreGetByCollaboratorIDWithAccessModeFunc{
defaultHook: i.GetByCollaboratorIDWithAccessMode,
},
+ GetByIDFunc: &ReposStoreGetByIDFunc{
+ defaultHook: i.GetByID,
+ },
GetByNameFunc: &ReposStoreGetByNameFunc{
defaultHook: i.GetByName,
},
+ StarFunc: &ReposStoreStarFunc{
+ defaultHook: i.Star,
+ },
TouchFunc: &ReposStoreTouchFunc{
defaultHook: i.Touch,
},
@@ -1934,6 +1966,114 @@ func (c ReposStoreGetByCollaboratorIDWithAccessModeFuncCall) Results() []interfa
return []interface{}{c.Result0, c.Result1}
}
+// ReposStoreGetByIDFunc describes the behavior when the GetByID method of
+// the parent MockReposStore instance is invoked.
+type ReposStoreGetByIDFunc struct {
+ defaultHook func(context.Context, int64) (*db.Repository, error)
+ hooks []func(context.Context, int64) (*db.Repository, error)
+ history []ReposStoreGetByIDFuncCall
+ mutex sync.Mutex
+}
+
+// GetByID delegates to the next hook function in the queue and stores the
+// parameter and result values of this invocation.
+func (m *MockReposStore) GetByID(v0 context.Context, v1 int64) (*db.Repository, error) {
+ r0, r1 := m.GetByIDFunc.nextHook()(v0, v1)
+ m.GetByIDFunc.appendCall(ReposStoreGetByIDFuncCall{v0, v1, r0, r1})
+ return r0, r1
+}
+
+// SetDefaultHook sets function that is called when the GetByID method of
+// the parent MockReposStore instance is invoked and the hook queue is
+// empty.
+func (f *ReposStoreGetByIDFunc) SetDefaultHook(hook func(context.Context, int64) (*db.Repository, error)) {
+ f.defaultHook = hook
+}
+
+// PushHook adds a function to the end of hook queue. Each invocation of the
+// GetByID method of the parent MockReposStore instance invokes the hook at
+// the front of the queue and discards it. After the queue is empty, the
+// default hook function is invoked for any future action.
+func (f *ReposStoreGetByIDFunc) PushHook(hook func(context.Context, int64) (*db.Repository, error)) {
+ f.mutex.Lock()
+ f.hooks = append(f.hooks, hook)
+ f.mutex.Unlock()
+}
+
+// SetDefaultReturn calls SetDefaultHook with a function that returns the
+// given values.
+func (f *ReposStoreGetByIDFunc) SetDefaultReturn(r0 *db.Repository, r1 error) {
+ f.SetDefaultHook(func(context.Context, int64) (*db.Repository, error) {
+ return r0, r1
+ })
+}
+
+// PushReturn calls PushHook with a function that returns the given values.
+func (f *ReposStoreGetByIDFunc) PushReturn(r0 *db.Repository, r1 error) {
+ f.PushHook(func(context.Context, int64) (*db.Repository, error) {
+ return r0, r1
+ })
+}
+
+func (f *ReposStoreGetByIDFunc) nextHook() func(context.Context, int64) (*db.Repository, error) {
+ f.mutex.Lock()
+ defer f.mutex.Unlock()
+
+ if len(f.hooks) == 0 {
+ return f.defaultHook
+ }
+
+ hook := f.hooks[0]
+ f.hooks = f.hooks[1:]
+ return hook
+}
+
+func (f *ReposStoreGetByIDFunc) appendCall(r0 ReposStoreGetByIDFuncCall) {
+ f.mutex.Lock()
+ f.history = append(f.history, r0)
+ f.mutex.Unlock()
+}
+
+// History returns a sequence of ReposStoreGetByIDFuncCall objects
+// describing the invocations of this function.
+func (f *ReposStoreGetByIDFunc) History() []ReposStoreGetByIDFuncCall {
+ f.mutex.Lock()
+ history := make([]ReposStoreGetByIDFuncCall, len(f.history))
+ copy(history, f.history)
+ f.mutex.Unlock()
+
+ return history
+}
+
+// ReposStoreGetByIDFuncCall is an object that describes an invocation of
+// method GetByID on an instance of MockReposStore.
+type ReposStoreGetByIDFuncCall struct {
+ // Arg0 is the value of the 1st argument passed to this method
+ // invocation.
+ Arg0 context.Context
+ // Arg1 is the value of the 2nd argument passed to this method
+ // invocation.
+ Arg1 int64
+ // Result0 is the value of the 1st result returned from this method
+ // invocation.
+ Result0 *db.Repository
+ // Result1 is the value of the 2nd result returned from this method
+ // invocation.
+ Result1 error
+}
+
+// Args returns an interface slice containing the arguments of this
+// invocation.
+func (c ReposStoreGetByIDFuncCall) Args() []interface{} {
+ return []interface{}{c.Arg0, c.Arg1}
+}
+
+// Results returns an interface slice containing the results of this
+// invocation.
+func (c ReposStoreGetByIDFuncCall) Results() []interface{} {
+ return []interface{}{c.Result0, c.Result1}
+}
+
// ReposStoreGetByNameFunc describes the behavior when the GetByName method
// of the parent MockReposStore instance is invoked.
type ReposStoreGetByNameFunc struct {
@@ -2045,6 +2185,113 @@ func (c ReposStoreGetByNameFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
+// ReposStoreStarFunc describes the behavior when the Star method of the
+// parent MockReposStore instance is invoked.
+type ReposStoreStarFunc struct {
+ defaultHook func(context.Context, int64, int64) error
+ hooks []func(context.Context, int64, int64) error
+ history []ReposStoreStarFuncCall
+ mutex sync.Mutex
+}
+
+// Star delegates to the next hook function in the queue and stores the
+// parameter and result values of this invocation.
+func (m *MockReposStore) Star(v0 context.Context, v1 int64, v2 int64) error {
+ r0 := m.StarFunc.nextHook()(v0, v1, v2)
+ m.StarFunc.appendCall(ReposStoreStarFuncCall{v0, v1, v2, r0})
+ return r0
+}
+
+// SetDefaultHook sets function that is called when the Star method of the
+// parent MockReposStore instance is invoked and the hook queue is empty.
+func (f *ReposStoreStarFunc) SetDefaultHook(hook func(context.Context, int64, int64) error) {
+ f.defaultHook = hook
+}
+
+// PushHook adds a function to the end of hook queue. Each invocation of the
+// Star method of the parent MockReposStore instance invokes the hook at the
+// front of the queue and discards it. After the queue is empty, the default
+// hook function is invoked for any future action.
+func (f *ReposStoreStarFunc) PushHook(hook func(context.Context, int64, int64) error) {
+ f.mutex.Lock()
+ f.hooks = append(f.hooks, hook)
+ f.mutex.Unlock()
+}
+
+// SetDefaultReturn calls SetDefaultHook with a function that returns the
+// given values.
+func (f *ReposStoreStarFunc) SetDefaultReturn(r0 error) {
+ f.SetDefaultHook(func(context.Context, int64, int64) error {
+ return r0
+ })
+}
+
+// PushReturn calls PushHook with a function that returns the given values.
+func (f *ReposStoreStarFunc) PushReturn(r0 error) {
+ f.PushHook(func(context.Context, int64, int64) error {
+ return r0
+ })
+}
+
+func (f *ReposStoreStarFunc) nextHook() func(context.Context, int64, int64) error {
+ f.mutex.Lock()
+ defer f.mutex.Unlock()
+
+ if len(f.hooks) == 0 {
+ return f.defaultHook
+ }
+
+ hook := f.hooks[0]
+ f.hooks = f.hooks[1:]
+ return hook
+}
+
+func (f *ReposStoreStarFunc) appendCall(r0 ReposStoreStarFuncCall) {
+ f.mutex.Lock()
+ f.history = append(f.history, r0)
+ f.mutex.Unlock()
+}
+
+// History returns a sequence of ReposStoreStarFuncCall objects describing
+// the invocations of this function.
+func (f *ReposStoreStarFunc) History() []ReposStoreStarFuncCall {
+ f.mutex.Lock()
+ history := make([]ReposStoreStarFuncCall, len(f.history))
+ copy(history, f.history)
+ f.mutex.Unlock()
+
+ return history
+}
+
+// ReposStoreStarFuncCall is an object that describes an invocation of
+// method Star on an instance of MockReposStore.
+type ReposStoreStarFuncCall struct {
+ // Arg0 is the value of the 1st argument passed to this method
+ // invocation.
+ Arg0 context.Context
+ // Arg1 is the value of the 2nd argument passed to this method
+ // invocation.
+ Arg1 int64
+ // Arg2 is the value of the 3rd argument passed to this method
+ // invocation.
+ Arg2 int64
+ // Result0 is the value of the 1st result returned from this method
+ // invocation.
+ Result0 error
+}
+
+// Args returns an interface slice containing the arguments of this
+// invocation.
+func (c ReposStoreStarFuncCall) Args() []interface{} {
+ return []interface{}{c.Arg0, c.Arg1, c.Arg2}
+}
+
+// Results returns an interface slice containing the results of this
+// invocation.
+func (c ReposStoreStarFuncCall) Results() []interface{} {
+ return []interface{}{c.Result0}
+}
+
// ReposStoreTouchFunc describes the behavior when the Touch method of the
// parent MockReposStore instance is invoked.
type ReposStoreTouchFunc struct {
@@ -2565,9 +2812,15 @@ type MockUsersStore struct {
// CreateFunc is an instance of a mock function object controlling the
// behavior of the method Create.
CreateFunc *UsersStoreCreateFunc
+ // DeleteByIDFunc is an instance of a mock function object controlling
+ // the behavior of the method DeleteByID.
+ DeleteByIDFunc *UsersStoreDeleteByIDFunc
// DeleteCustomAvatarFunc is an instance of a mock function object
// controlling the behavior of the method DeleteCustomAvatar.
DeleteCustomAvatarFunc *UsersStoreDeleteCustomAvatarFunc
+ // DeleteInactivatedFunc is an instance of a mock function object
+ // controlling the behavior of the method DeleteInactivated.
+ DeleteInactivatedFunc *UsersStoreDeleteInactivatedFunc
// GetByEmailFunc is an instance of a mock function object controlling
// the behavior of the method GetByEmail.
GetByEmailFunc *UsersStoreGetByEmailFunc
@@ -2634,11 +2887,21 @@ func NewMockUsersStore() *MockUsersStore {
return
},
},
+ DeleteByIDFunc: &UsersStoreDeleteByIDFunc{
+ defaultHook: func(context.Context, int64, bool) (r0 error) {
+ return
+ },
+ },
DeleteCustomAvatarFunc: &UsersStoreDeleteCustomAvatarFunc{
defaultHook: func(context.Context, int64) (r0 error) {
return
},
},
+ DeleteInactivatedFunc: &UsersStoreDeleteInactivatedFunc{
+ defaultHook: func() (r0 error) {
+ return
+ },
+ },
GetByEmailFunc: &UsersStoreGetByEmailFunc{
defaultHook: func(context.Context, string) (r0 *db.User, r1 error) {
return
@@ -2731,11 +2994,21 @@ func NewStrictMockUsersStore() *MockUsersStore {
panic("unexpected invocation of MockUsersStore.Create")
},
},
+ DeleteByIDFunc: &UsersStoreDeleteByIDFunc{
+ defaultHook: func(context.Context, int64, bool) error {
+ panic("unexpected invocation of MockUsersStore.DeleteByID")
+ },
+ },
DeleteCustomAvatarFunc: &UsersStoreDeleteCustomAvatarFunc{
defaultHook: func(context.Context, int64) error {
panic("unexpected invocation of MockUsersStore.DeleteCustomAvatar")
},
},
+ DeleteInactivatedFunc: &UsersStoreDeleteInactivatedFunc{
+ defaultHook: func() error {
+ panic("unexpected invocation of MockUsersStore.DeleteInactivated")
+ },
+ },
GetByEmailFunc: &UsersStoreGetByEmailFunc{
defaultHook: func(context.Context, string) (*db.User, error) {
panic("unexpected invocation of MockUsersStore.GetByEmail")
@@ -2820,9 +3093,15 @@ func NewMockUsersStoreFrom(i db.UsersStore) *MockUsersStore {
CreateFunc: &UsersStoreCreateFunc{
defaultHook: i.Create,
},
+ DeleteByIDFunc: &UsersStoreDeleteByIDFunc{
+ defaultHook: i.DeleteByID,
+ },
DeleteCustomAvatarFunc: &UsersStoreDeleteCustomAvatarFunc{
defaultHook: i.DeleteCustomAvatar,
},
+ DeleteInactivatedFunc: &UsersStoreDeleteInactivatedFunc{
+ defaultHook: i.DeleteInactivated,
+ },
GetByEmailFunc: &UsersStoreGetByEmailFunc{
defaultHook: i.GetByEmail,
},
@@ -3301,6 +3580,114 @@ func (c UsersStoreCreateFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
+// UsersStoreDeleteByIDFunc describes the behavior when the DeleteByID
+// method of the parent MockUsersStore instance is invoked.
+type UsersStoreDeleteByIDFunc struct {
+ defaultHook func(context.Context, int64, bool) error
+ hooks []func(context.Context, int64, bool) error
+ history []UsersStoreDeleteByIDFuncCall
+ mutex sync.Mutex
+}
+
+// DeleteByID delegates to the next hook function in the queue and stores
+// the parameter and result values of this invocation.
+func (m *MockUsersStore) DeleteByID(v0 context.Context, v1 int64, v2 bool) error {
+ r0 := m.DeleteByIDFunc.nextHook()(v0, v1, v2)
+ m.DeleteByIDFunc.appendCall(UsersStoreDeleteByIDFuncCall{v0, v1, v2, r0})
+ return r0
+}
+
+// SetDefaultHook sets function that is called when the DeleteByID method of
+// the parent MockUsersStore instance is invoked and the hook queue is
+// empty.
+func (f *UsersStoreDeleteByIDFunc) SetDefaultHook(hook func(context.Context, int64, bool) error) {
+ f.defaultHook = hook
+}
+
+// PushHook adds a function to the end of hook queue. Each invocation of the
+// DeleteByID method of the parent MockUsersStore instance invokes the hook
+// at the front of the queue and discards it. After the queue is empty, the
+// default hook function is invoked for any future action.
+func (f *UsersStoreDeleteByIDFunc) PushHook(hook func(context.Context, int64, bool) error) {
+ f.mutex.Lock()
+ f.hooks = append(f.hooks, hook)
+ f.mutex.Unlock()
+}
+
+// SetDefaultReturn calls SetDefaultHook with a function that returns the
+// given values.
+func (f *UsersStoreDeleteByIDFunc) SetDefaultReturn(r0 error) {
+ f.SetDefaultHook(func(context.Context, int64, bool) error {
+ return r0
+ })
+}
+
+// PushReturn calls PushHook with a function that returns the given values.
+func (f *UsersStoreDeleteByIDFunc) PushReturn(r0 error) {
+ f.PushHook(func(context.Context, int64, bool) error {
+ return r0
+ })
+}
+
+func (f *UsersStoreDeleteByIDFunc) nextHook() func(context.Context, int64, bool) error {
+ f.mutex.Lock()
+ defer f.mutex.Unlock()
+
+ if len(f.hooks) == 0 {
+ return f.defaultHook
+ }
+
+ hook := f.hooks[0]
+ f.hooks = f.hooks[1:]
+ return hook
+}
+
+func (f *UsersStoreDeleteByIDFunc) appendCall(r0 UsersStoreDeleteByIDFuncCall) {
+ f.mutex.Lock()
+ f.history = append(f.history, r0)
+ f.mutex.Unlock()
+}
+
+// History returns a sequence of UsersStoreDeleteByIDFuncCall objects
+// describing the invocations of this function.
+func (f *UsersStoreDeleteByIDFunc) History() []UsersStoreDeleteByIDFuncCall {
+ f.mutex.Lock()
+ history := make([]UsersStoreDeleteByIDFuncCall, len(f.history))
+ copy(history, f.history)
+ f.mutex.Unlock()
+
+ return history
+}
+
+// UsersStoreDeleteByIDFuncCall is an object that describes an invocation of
+// method DeleteByID on an instance of MockUsersStore.
+type UsersStoreDeleteByIDFuncCall struct {
+ // Arg0 is the value of the 1st argument passed to this method
+ // invocation.
+ Arg0 context.Context
+ // Arg1 is the value of the 2nd argument passed to this method
+ // invocation.
+ Arg1 int64
+ // Arg2 is the value of the 3rd argument passed to this method
+ // invocation.
+ Arg2 bool
+ // Result0 is the value of the 1st result returned from this method
+ // invocation.
+ Result0 error
+}
+
+// Args returns an interface slice containing the arguments of this
+// invocation.
+func (c UsersStoreDeleteByIDFuncCall) Args() []interface{} {
+ return []interface{}{c.Arg0, c.Arg1, c.Arg2}
+}
+
+// Results returns an interface slice containing the results of this
+// invocation.
+func (c UsersStoreDeleteByIDFuncCall) Results() []interface{} {
+ return []interface{}{c.Result0}
+}
+
// UsersStoreDeleteCustomAvatarFunc describes the behavior when the
// DeleteCustomAvatar method of the parent MockUsersStore instance is
// invoked.
@@ -3407,6 +3794,106 @@ func (c UsersStoreDeleteCustomAvatarFuncCall) Results() []interface{} {
return []interface{}{c.Result0}
}
+// UsersStoreDeleteInactivatedFunc describes the behavior when the
+// DeleteInactivated method of the parent MockUsersStore instance is
+// invoked.
+type UsersStoreDeleteInactivatedFunc struct {
+ defaultHook func() error
+ hooks []func() error
+ history []UsersStoreDeleteInactivatedFuncCall
+ mutex sync.Mutex
+}
+
+// DeleteInactivated delegates to the next hook function in the queue and
+// stores the parameter and result values of this invocation.
+func (m *MockUsersStore) DeleteInactivated() error {
+ r0 := m.DeleteInactivatedFunc.nextHook()()
+ m.DeleteInactivatedFunc.appendCall(UsersStoreDeleteInactivatedFuncCall{r0})
+ return r0
+}
+
+// SetDefaultHook sets function that is called when the DeleteInactivated
+// method of the parent MockUsersStore instance is invoked and the hook
+// queue is empty.
+func (f *UsersStoreDeleteInactivatedFunc) SetDefaultHook(hook func() error) {
+ f.defaultHook = hook
+}
+
+// PushHook adds a function to the end of hook queue. Each invocation of the
+// DeleteInactivated method of the parent MockUsersStore instance invokes
+// the hook at the front of the queue and discards it. After the queue is
+// empty, the default hook function is invoked for any future action.
+func (f *UsersStoreDeleteInactivatedFunc) PushHook(hook func() error) {
+ f.mutex.Lock()
+ f.hooks = append(f.hooks, hook)
+ f.mutex.Unlock()
+}
+
+// SetDefaultReturn calls SetDefaultHook with a function that returns the
+// given values.
+func (f *UsersStoreDeleteInactivatedFunc) SetDefaultReturn(r0 error) {
+ f.SetDefaultHook(func() error {
+ return r0
+ })
+}
+
+// PushReturn calls PushHook with a function that returns the given values.
+func (f *UsersStoreDeleteInactivatedFunc) PushReturn(r0 error) {
+ f.PushHook(func() error {
+ return r0
+ })
+}
+
+func (f *UsersStoreDeleteInactivatedFunc) nextHook() func() error {
+ f.mutex.Lock()
+ defer f.mutex.Unlock()
+
+ if len(f.hooks) == 0 {
+ return f.defaultHook
+ }
+
+ hook := f.hooks[0]
+ f.hooks = f.hooks[1:]
+ return hook
+}
+
+func (f *UsersStoreDeleteInactivatedFunc) appendCall(r0 UsersStoreDeleteInactivatedFuncCall) {
+ f.mutex.Lock()
+ f.history = append(f.history, r0)
+ f.mutex.Unlock()
+}
+
+// History returns a sequence of UsersStoreDeleteInactivatedFuncCall objects
+// describing the invocations of this function.
+func (f *UsersStoreDeleteInactivatedFunc) History() []UsersStoreDeleteInactivatedFuncCall {
+ f.mutex.Lock()
+ history := make([]UsersStoreDeleteInactivatedFuncCall, len(f.history))
+ copy(history, f.history)
+ f.mutex.Unlock()
+
+ return history
+}
+
+// UsersStoreDeleteInactivatedFuncCall is an object that describes an
+// invocation of method DeleteInactivated on an instance of MockUsersStore.
+type UsersStoreDeleteInactivatedFuncCall struct {
+ // Result0 is the value of the 1st result returned from this method
+ // invocation.
+ Result0 error
+}
+
+// Args returns an interface slice containing the arguments of this
+// invocation.
+func (c UsersStoreDeleteInactivatedFuncCall) Args() []interface{} {
+ return []interface{}{}
+}
+
+// Results returns an interface slice containing the results of this
+// invocation.
+func (c UsersStoreDeleteInactivatedFuncCall) Results() []interface{} {
+ return []interface{}{c.Result0}
+}
+
// UsersStoreGetByEmailFunc describes the behavior when the GetByEmail
// method of the parent MockUsersStore instance is invoked.
type UsersStoreGetByEmailFunc struct {
diff --git a/internal/route/user/setting.go b/internal/route/user/setting.go
index fffefe1d..82aceaf2 100644
--- a/internal/route/user/setting.go
+++ b/internal/route/user/setting.go
@@ -649,7 +649,7 @@ func SettingsDelete(c *context.Context) {
return
}
- if err := db.DeleteUser(c.User); err != nil {
+ if err := db.Users.DeleteByID(c.Req.Context(), c.User.ID, false); err != nil {
switch {
case db.IsErrUserOwnRepos(err):
c.Flash.Error(c.Tr("form.still_own_repo"))