Răsfoiți Sursa

refactor(db): migrate `ChangeUsername` off `user.go` (#7261)

Joe Chen 2 ani în urmă
părinte
comite
644a3a9d78

+ 9 - 0
internal/conf/mocks.go

@@ -5,6 +5,7 @@
 package conf
 
 import (
+	"sync"
 	"testing"
 )
 
@@ -24,11 +25,15 @@ func SetMockAuth(t *testing.T, otps AuthOpts) {
 	})
 }
 
+var mockServer sync.Mutex
+
 func SetMockServer(t *testing.T, opts ServerOpts) {
+	mockServer.Lock()
 	before := Server
 	Server = opts
 	t.Cleanup(func() {
 		Server = before
+		mockServer.Unlock()
 	})
 }
 
@@ -40,11 +45,15 @@ func SetMockSSH(t *testing.T, opts SSHOpts) {
 	})
 }
 
+var mockRepository sync.Mutex
+
 func SetMockRepository(t *testing.T, opts RepositoryOpts) {
+	mockRepository.Lock()
 	before := Repository
 	Repository = opts
 	t.Cleanup(func() {
 		Repository = before
+		mockRepository.Unlock()
 	})
 }
 

+ 9 - 18
internal/db/pull.go

@@ -9,7 +9,6 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
-	"strings"
 	"time"
 
 	"github.com/unknwon/com"
@@ -45,28 +44,28 @@ const (
 
 // PullRequest represents relation between pull request and repositories.
 type PullRequest struct {
-	ID     int64
+	ID     int64 `gorm:"primaryKey"`
 	Type   PullRequestType
 	Status PullRequestStatus
 
-	IssueID int64  `xorm:"INDEX"`
-	Issue   *Issue `xorm:"-" json:"-"`
+	IssueID int64  `xorm:"INDEX" gorm:"index"`
+	Issue   *Issue `xorm:"-" json:"-" gorm:"-"`
 	Index   int64
 
 	HeadRepoID   int64
-	HeadRepo     *Repository `xorm:"-" json:"-"`
+	HeadRepo     *Repository `xorm:"-" json:"-" gorm:"-"`
 	BaseRepoID   int64
-	BaseRepo     *Repository `xorm:"-" json:"-"`
+	BaseRepo     *Repository `xorm:"-" json:"-" gorm:"-"`
 	HeadUserName string
 	HeadBranch   string
 	BaseBranch   string
-	MergeBase    string `xorm:"VARCHAR(40)"`
+	MergeBase    string `xorm:"VARCHAR(40)" gorm:"type:VARCHAR(40)"`
 
 	HasMerged      bool
-	MergedCommitID string `xorm:"VARCHAR(40)"`
+	MergedCommitID string `xorm:"VARCHAR(40)" gorm:"type:VARCHAR(40)"`
 	MergerID       int64
-	Merger         *User     `xorm:"-" json:"-"`
-	Merged         time.Time `xorm:"-" json:"-"`
+	Merger         *User     `xorm:"-" json:"-" gorm:"-"`
+	Merged         time.Time `xorm:"-" json:"-" gorm:"-"`
 	MergedUnix     int64
 }
 
@@ -823,14 +822,6 @@ func AddTestPullRequestTask(doer *User, repoID int64, branch string, isSync bool
 	}
 }
 
-func ChangeUsernameInPullRequests(oldUserName, newUserName string) error {
-	pr := PullRequest{
-		HeadUserName: strings.ToLower(newUserName),
-	}
-	_, err := x.Cols("head_user_name").Where("head_user_name = ?", strings.ToLower(oldUserName)).Update(pr)
-	return err
-}
-
 // checkAndUpdateStatus checks if pull request is possible to leaving checking status,
 // and set to be either conflict or mergeable.
 func (pr *PullRequest) checkAndUpdateStatus() {

+ 6 - 6
internal/db/repo.go

@@ -1444,7 +1444,7 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error
 		return fmt.Errorf("rename repository directory: %v", err)
 	}
 
-	deleteRepoLocalCopy(repo)
+	deleteRepoLocalCopy(repo.ID)
 
 	// Rename remote wiki repository to new path and delete local copy.
 	wikiPath := WikiPath(owner.Name, repo.Name)
@@ -1458,10 +1458,10 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error
 	return sess.Commit()
 }
 
-func deleteRepoLocalCopy(repo *Repository) {
-	repoWorkingPool.CheckIn(com.ToStr(repo.ID))
-	defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
-	RemoveAllWithNotice("Delete repository local copy", repo.LocalCopyPath())
+func deleteRepoLocalCopy(repoID int64) {
+	repoWorkingPool.CheckIn(com.ToStr(repoID))
+	defer repoWorkingPool.CheckOut(com.ToStr(repoID))
+	RemoveAllWithNotice(fmt.Sprintf("Delete repository %d local copy", repoID), repoutil.RepositoryLocalPath(repoID))
 }
 
 // ChangeRepositoryName changes all corresponding setting from old repository name to new one.
@@ -1497,7 +1497,7 @@ func ChangeRepositoryName(u *User, oldRepoName, newRepoName string) (err error)
 		RemoveAllWithNotice("Delete repository wiki local copy", repo.LocalWikiPath())
 	}
 
-	deleteRepoLocalCopy(repo)
+	deleteRepoLocalCopy(repo.ID)
 	return nil
 }
 

+ 0 - 35
internal/db/user.go

@@ -12,7 +12,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/unknwon/com"
 	log "unknwon.dev/clog/v2"
 	"xorm.io/xorm"
 
@@ -57,40 +56,6 @@ func (u *User) getOrganizationCount(e Engine) (int64, error) {
 	return e.Where("uid=?", u.ID).Count(new(OrgUser))
 }
 
-// ChangeUserName changes all corresponding setting from old user name to new one.
-func ChangeUserName(u *User, newUserName string) (err error) {
-	if err = isUsernameAllowed(newUserName); err != nil {
-		return err
-	}
-
-	if Users.IsUsernameUsed(context.TODO(), newUserName) {
-		return ErrUserAlreadyExist{args: errutil.Args{"name": newUserName}}
-	}
-
-	if err = ChangeUsernameInPullRequests(u.Name, newUserName); err != nil {
-		return fmt.Errorf("ChangeUsernameInPullRequests: %v", err)
-	}
-
-	// Delete all local copies of repositories and wikis the user owns.
-	if err = x.Where("owner_id=?", u.ID).Iterate(new(Repository), func(idx int, bean interface{}) error {
-		repo := bean.(*Repository)
-		deleteRepoLocalCopy(repo)
-		// TODO: By the same reasoning, shouldn't we also sync access to the local wiki path?
-		RemoveAllWithNotice("Delete repository wiki local copy", repo.LocalWikiPath())
-		return nil
-	}); err != nil {
-		return fmt.Errorf("delete repository and wiki local copy: %v", err)
-	}
-
-	// Rename or create user base directory
-	baseDir := repoutil.UserPath(u.Name)
-	newBaseDir := repoutil.UserPath(newUserName)
-	if com.IsExist(baseDir) {
-		return os.Rename(baseDir, newBaseDir)
-	}
-	return os.MkdirAll(newBaseDir, os.ModePerm)
-}
-
 func updateUser(e Engine, u *User) error {
 	// Organization does not need email
 	if !u.IsOrganization() {

+ 84 - 2
internal/db/users.go

@@ -24,6 +24,7 @@ import (
 	"gogs.io/gogs/internal/dbutil"
 	"gogs.io/gogs/internal/errutil"
 	"gogs.io/gogs/internal/osutil"
+	"gogs.io/gogs/internal/repoutil"
 	"gogs.io/gogs/internal/strutil"
 	"gogs.io/gogs/internal/tool"
 	"gogs.io/gogs/internal/userutil"
@@ -45,11 +46,17 @@ type UsersStore interface {
 	// When the "loginSourceID" is positive, it tries to authenticate via given
 	// login source and creates a new user when not yet exists in the database.
 	Authenticate(ctx context.Context, username, password string, loginSourceID int64) (*User, error)
+	// ChangeUsername changes the username of the given user and updates all
+	// references to the old username. It returns ErrNameNotAllowed if the given
+	// name or pattern of the name is not allowed as a username, or
+	// ErrUserAlreadyExist when another user with same name already exists.
+	ChangeUsername(ctx context.Context, userID int64, newUsername string) error
 	// Count returns the total number of users.
 	Count(ctx context.Context) int64
 	// Create creates a new user and persists to database. It returns
-	// ErrUserAlreadyExist when a user with same name already exists, or
-	// ErrEmailAlreadyUsed if the email has been used by another user.
+	// ErrNameNotAllowed if the given name or pattern of the name is not allowed as
+	// a username, or ErrUserAlreadyExist when a user with same name already exists,
+	// or ErrEmailAlreadyUsed if the email has been used by another user.
 	Create(ctx context.Context, username, email string, opts CreateUserOptions) (*User, error)
 	// DeleteCustomAvatar deletes the current user custom avatar and falls back to
 	// use look up avatar by email.
@@ -188,6 +195,81 @@ func (db *users) Authenticate(ctx context.Context, login, password string, login
 	)
 }
 
+func (db *users) ChangeUsername(ctx context.Context, userID int64, newUsername string) error {
+	err := isUsernameAllowed(newUsername)
+	if err != nil {
+		return err
+	}
+
+	if db.IsUsernameUsed(ctx, newUsername) {
+		return ErrUserAlreadyExist{
+			args: errutil.Args{
+				"name": newUsername,
+			},
+		}
+	}
+
+	user, err := db.GetByID(ctx, userID)
+	if err != nil {
+		return errors.Wrap(err, "get user")
+	}
+
+	return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+		err := tx.Model(&User{}).
+			Where("id = ?", user.ID).
+			Updates(map[string]any{
+				"lower_name": strings.ToLower(newUsername),
+				"name":       newUsername,
+			}).Error
+		if err != nil {
+			return errors.Wrap(err, "update user name")
+		}
+
+		// Update all references to the user name in pull requests
+		err = tx.Model(&PullRequest{}).
+			Where("head_user_name = ?", user.LowerName).
+			Update("head_user_name", strings.ToLower(newUsername)).
+			Error
+		if err != nil {
+			return errors.Wrap(err, `update "pull_request.head_user_name"`)
+		}
+
+		// Delete local copies of repositories and their wikis that are owned by the user
+		rows, err := tx.Model(&Repository{}).Where("owner_id = ?", user.ID).Rows()
+		if err != nil {
+			return errors.Wrap(err, "iterate repositories")
+		}
+		defer func() { _ = rows.Close() }()
+
+		for rows.Next() {
+			var repo struct {
+				ID int64
+			}
+			err = tx.ScanRows(rows, &repo)
+			if err != nil {
+				return errors.Wrap(err, "scan rows")
+			}
+
+			deleteRepoLocalCopy(repo.ID)
+			RemoveAllWithNotice(fmt.Sprintf("Delete repository %d wiki local copy", repo.ID), repoutil.RepositoryLocalWikiPath(repo.ID))
+		}
+		if err = rows.Err(); err != nil {
+			return errors.Wrap(err, "check rows.Err")
+		}
+
+		// Rename user directory if exists
+		userPath := repoutil.UserPath(user.Name)
+		if osutil.IsExist(userPath) {
+			newUserPath := repoutil.UserPath(newUsername)
+			err = os.Rename(userPath, newUserPath)
+			if err != nil {
+				return errors.Wrap(err, "rename user directory")
+			}
+		}
+		return nil
+	})
+}
+
 func (db *users) Count(ctx context.Context) int64 {
 	var count int64
 	db.WithContext(ctx).Model(&User{}).Where("type = ?", UserTypeIndividual).Count(&count)

+ 106 - 1
internal/db/users_test.go

@@ -8,6 +8,7 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"path/filepath"
 	"strings"
 	"testing"
 	"time"
@@ -17,10 +18,12 @@ import (
 	"gorm.io/gorm"
 
 	"gogs.io/gogs/internal/auth"
+	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/dbtest"
 	"gogs.io/gogs/internal/dbutil"
 	"gogs.io/gogs/internal/errutil"
 	"gogs.io/gogs/internal/osutil"
+	"gogs.io/gogs/internal/repoutil"
 	"gogs.io/gogs/internal/userutil"
 	"gogs.io/gogs/public"
 )
@@ -79,7 +82,7 @@ func TestUsers(t *testing.T) {
 	}
 	t.Parallel()
 
-	tables := []interface{}{new(User), new(EmailAddress), new(Repository), new(Follow)}
+	tables := []interface{}{new(User), new(EmailAddress), new(Repository), new(Follow), new(PullRequest)}
 	db := &users{
 		DB: dbtest.NewDB(t, "users", tables...),
 	}
@@ -89,6 +92,7 @@ func TestUsers(t *testing.T) {
 		test func(t *testing.T, db *users)
 	}{
 		{"Authenticate", usersAuthenticate},
+		{"ChangeUsername", usersChangeUsername},
 		{"Count", usersCount},
 		{"Create", usersCreate},
 		{"DeleteCustomAvatar", usersDeleteCustomAvatar},
@@ -212,6 +216,107 @@ func usersAuthenticate(t *testing.T, db *users) {
 	})
 }
 
+func usersChangeUsername(t *testing.T, db *users) {
+	ctx := context.Background()
+
+	alice, err := db.Create(
+		ctx,
+		"alice",
+		"[email protected]",
+		CreateUserOptions{
+			Activated: true,
+		},
+	)
+	require.NoError(t, err)
+
+	t.Run("name not allowed", func(t *testing.T) {
+		err := db.ChangeUsername(ctx, alice.ID, "-")
+		wantErr := ErrNameNotAllowed{
+			args: errutil.Args{
+				"reason": "reserved",
+				"name":   "-",
+			},
+		}
+		assert.Equal(t, wantErr, err)
+	})
+
+	t.Run("name already exists", func(t *testing.T) {
+		err := db.ChangeUsername(ctx, alice.ID, alice.Name)
+		wantErr := ErrUserAlreadyExist{
+			args: errutil.Args{
+				"name": alice.Name,
+			},
+		}
+		assert.Equal(t, wantErr, err)
+	})
+
+	tempRepositoryRoot := filepath.Join(os.TempDir(), "usersChangeUsername-tempRepositoryRoot")
+	conf.SetMockRepository(
+		t,
+		conf.RepositoryOpts{
+			Root: tempRepositoryRoot,
+		},
+	)
+	err = os.RemoveAll(tempRepositoryRoot)
+	require.NoError(t, err)
+	defer func() { _ = os.RemoveAll(tempRepositoryRoot) }()
+
+	tempServerAppDataPath := filepath.Join(os.TempDir(), "usersChangeUsername-tempServerAppDataPath")
+	conf.SetMockServer(
+		t,
+		conf.ServerOpts{
+			AppDataPath: tempServerAppDataPath,
+		},
+	)
+	err = os.RemoveAll(tempServerAppDataPath)
+	require.NoError(t, err)
+	defer func() { _ = os.RemoveAll(tempServerAppDataPath) }()
+
+	repo, err := NewReposStore(db.DB).Create(
+		ctx,
+		alice.ID,
+		CreateRepoOptions{
+			Name: "test-repo-1",
+		},
+	)
+	require.NoError(t, err)
+
+	// TODO: Use PullRequests.Create to replace SQL hack when the method is available.
+	err = db.Exec(`INSERT INTO pull_request (head_user_name) VALUES (?)`, alice.Name).Error
+	require.NoError(t, err)
+
+	err = os.MkdirAll(repoutil.UserPath(alice.Name), os.ModePerm)
+	require.NoError(t, err)
+	err = os.MkdirAll(repoutil.RepositoryLocalPath(repo.ID), os.ModePerm)
+	require.NoError(t, err)
+	err = os.MkdirAll(repoutil.RepositoryLocalWikiPath(repo.ID), os.ModePerm)
+	require.NoError(t, err)
+
+	// Make sure mock data is set up correctly
+	assert.True(t, osutil.IsExist(repoutil.UserPath(alice.Name)))
+	assert.True(t, osutil.IsExist(repoutil.RepositoryLocalPath(repo.ID)))
+	assert.True(t, osutil.IsExist(repoutil.RepositoryLocalWikiPath(repo.ID)))
+
+	const newUsername = "alice-new"
+	err = db.ChangeUsername(ctx, alice.ID, newUsername)
+	require.NoError(t, err)
+
+	// TODO: Use PullRequests.GetByID to replace SQL hack when the method is available.
+	var headUserName string
+	err = db.Select("head_user_name").Table("pull_request").Row().Scan(&headUserName)
+	require.NoError(t, err)
+	assert.Equal(t, headUserName, newUsername)
+
+	assert.True(t, osutil.IsExist(repoutil.UserPath(newUsername)))
+	assert.False(t, osutil.IsExist(repoutil.UserPath(alice.Name)))
+	assert.False(t, osutil.IsExist(repoutil.RepositoryLocalPath(repo.ID)))
+	assert.False(t, osutil.IsExist(repoutil.RepositoryLocalWikiPath(repo.ID)))
+
+	alice, err = db.GetByID(ctx, alice.ID)
+	require.NoError(t, err)
+	assert.Equal(t, newUsername, alice.Name)
+}
+
 func usersCount(t *testing.T, db *users) {
 	ctx := context.Background()
 

+ 13 - 0
internal/repoutil/repoutil.go

@@ -7,6 +7,7 @@ package repoutil
 import (
 	"fmt"
 	"path/filepath"
+	"strconv"
 	"strings"
 
 	"gogs.io/gogs/internal/conf"
@@ -60,3 +61,15 @@ func UserPath(user string) string {
 func RepositoryPath(owner, repo string) string {
 	return filepath.Join(UserPath(owner), strings.ToLower(repo)+".git")
 }
+
+// RepositoryLocalPath returns the absolute path of the repository local copy
+// with the given ID.
+func RepositoryLocalPath(repoID int64) string {
+	return filepath.Join(conf.Server.AppDataPath, "tmp", "local-repo", strconv.FormatInt(repoID, 10))
+}
+
+// RepositoryLocalWikiPath returns the absolute path of the repository local
+// wiki copy with the given ID.
+func RepositoryLocalWikiPath(repoID int64) string {
+	return filepath.Join(conf.Server.AppDataPath, "tmp", "local-wiki", strconv.FormatInt(repoID, 10))
+}

+ 36 - 0
internal/repoutil/repoutil_test.go

@@ -125,3 +125,39 @@ func TestRepositoryPath(t *testing.T) {
 	want := "/home/git/gogs-repositories/alice/example.git"
 	assert.Equal(t, want, got)
 }
+
+func TestRepositoryLocalPath(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("Skipping testing on Windows")
+		return
+	}
+
+	conf.SetMockServer(
+		t,
+		conf.ServerOpts{
+			AppDataPath: "data",
+		},
+	)
+
+	got := RepositoryLocalPath(1)
+	want := "data/tmp/local-repo/1"
+	assert.Equal(t, want, got)
+}
+
+func TestRepositoryLocalWikiPath(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("Skipping testing on Windows")
+		return
+	}
+
+	conf.SetMockServer(
+		t,
+		conf.ServerOpts{
+			AppDataPath: "data",
+		},
+	)
+
+	got := RepositoryLocalWikiPath(1)
+	want := "data/tmp/local-wiki/1"
+	assert.Equal(t, want, got)
+}

+ 124 - 0
internal/route/lfs/mocks_test.go

@@ -2295,6 +2295,9 @@ type MockUsersStore struct {
 	// AuthenticateFunc is an instance of a mock function object controlling
 	// the behavior of the method Authenticate.
 	AuthenticateFunc *UsersStoreAuthenticateFunc
+	// ChangeUsernameFunc is an instance of a mock function object
+	// controlling the behavior of the method ChangeUsername.
+	ChangeUsernameFunc *UsersStoreChangeUsernameFunc
 	// CountFunc is an instance of a mock function object controlling the
 	// behavior of the method Count.
 	CountFunc *UsersStoreCountFunc
@@ -2342,6 +2345,11 @@ func NewMockUsersStore() *MockUsersStore {
 				return
 			},
 		},
+		ChangeUsernameFunc: &UsersStoreChangeUsernameFunc{
+			defaultHook: func(context.Context, int64, string) (r0 error) {
+				return
+			},
+		},
 		CountFunc: &UsersStoreCountFunc{
 			defaultHook: func(context.Context) (r0 int64) {
 				return
@@ -2414,6 +2422,11 @@ func NewStrictMockUsersStore() *MockUsersStore {
 				panic("unexpected invocation of MockUsersStore.Authenticate")
 			},
 		},
+		ChangeUsernameFunc: &UsersStoreChangeUsernameFunc{
+			defaultHook: func(context.Context, int64, string) error {
+				panic("unexpected invocation of MockUsersStore.ChangeUsername")
+			},
+		},
 		CountFunc: &UsersStoreCountFunc{
 			defaultHook: func(context.Context) int64 {
 				panic("unexpected invocation of MockUsersStore.Count")
@@ -2484,6 +2497,9 @@ func NewMockUsersStoreFrom(i db.UsersStore) *MockUsersStore {
 		AuthenticateFunc: &UsersStoreAuthenticateFunc{
 			defaultHook: i.Authenticate,
 		},
+		ChangeUsernameFunc: &UsersStoreChangeUsernameFunc{
+			defaultHook: i.ChangeUsername,
+		},
 		CountFunc: &UsersStoreCountFunc{
 			defaultHook: i.Count,
 		},
@@ -2637,6 +2653,114 @@ func (c UsersStoreAuthenticateFuncCall) Results() []interface{} {
 	return []interface{}{c.Result0, c.Result1}
 }
 
+// UsersStoreChangeUsernameFunc describes the behavior when the
+// ChangeUsername method of the parent MockUsersStore instance is invoked.
+type UsersStoreChangeUsernameFunc struct {
+	defaultHook func(context.Context, int64, string) error
+	hooks       []func(context.Context, int64, string) error
+	history     []UsersStoreChangeUsernameFuncCall
+	mutex       sync.Mutex
+}
+
+// ChangeUsername delegates to the next hook function in the queue and
+// stores the parameter and result values of this invocation.
+func (m *MockUsersStore) ChangeUsername(v0 context.Context, v1 int64, v2 string) error {
+	r0 := m.ChangeUsernameFunc.nextHook()(v0, v1, v2)
+	m.ChangeUsernameFunc.appendCall(UsersStoreChangeUsernameFuncCall{v0, v1, v2, r0})
+	return r0
+}
+
+// SetDefaultHook sets function that is called when the ChangeUsername
+// method of the parent MockUsersStore instance is invoked and the hook
+// queue is empty.
+func (f *UsersStoreChangeUsernameFunc) SetDefaultHook(hook func(context.Context, int64, string) error) {
+	f.defaultHook = hook
+}
+
+// PushHook adds a function to the end of hook queue. Each invocation of the
+// ChangeUsername 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 *UsersStoreChangeUsernameFunc) PushHook(hook func(context.Context, int64, string) 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 *UsersStoreChangeUsernameFunc) SetDefaultReturn(r0 error) {
+	f.SetDefaultHook(func(context.Context, int64, string) error {
+		return r0
+	})
+}
+
+// PushReturn calls PushHook with a function that returns the given values.
+func (f *UsersStoreChangeUsernameFunc) PushReturn(r0 error) {
+	f.PushHook(func(context.Context, int64, string) error {
+		return r0
+	})
+}
+
+func (f *UsersStoreChangeUsernameFunc) nextHook() func(context.Context, int64, string) 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 *UsersStoreChangeUsernameFunc) appendCall(r0 UsersStoreChangeUsernameFuncCall) {
+	f.mutex.Lock()
+	f.history = append(f.history, r0)
+	f.mutex.Unlock()
+}
+
+// History returns a sequence of UsersStoreChangeUsernameFuncCall objects
+// describing the invocations of this function.
+func (f *UsersStoreChangeUsernameFunc) History() []UsersStoreChangeUsernameFuncCall {
+	f.mutex.Lock()
+	history := make([]UsersStoreChangeUsernameFuncCall, len(f.history))
+	copy(history, f.history)
+	f.mutex.Unlock()
+
+	return history
+}
+
+// UsersStoreChangeUsernameFuncCall is an object that describes an
+// invocation of method ChangeUsername on an instance of MockUsersStore.
+type UsersStoreChangeUsernameFuncCall 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 string
+	// 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 UsersStoreChangeUsernameFuncCall) Args() []interface{} {
+	return []interface{}{c.Arg0, c.Arg1, c.Arg2}
+}
+
+// Results returns an interface slice containing the results of this
+// invocation.
+func (c UsersStoreChangeUsernameFuncCall) Results() []interface{} {
+	return []interface{}{c.Result0}
+}
+
 // UsersStoreCountFunc describes the behavior when the Count method of the
 // parent MockUsersStore instance is invoked.
 type UsersStoreCountFunc struct {

+ 4 - 3
internal/route/org/setting.go

@@ -7,6 +7,7 @@ package org
 import (
 	"strings"
 
+	"github.com/pkg/errors"
 	log "unknwon.dev/clog/v2"
 
 	"gogs.io/gogs/internal/auth"
@@ -45,13 +46,13 @@ func SettingsPost(c *context.Context, f form.UpdateOrgSetting) {
 			c.Data["OrgName"] = true
 			c.RenderWithErr(c.Tr("form.username_been_taken"), SETTINGS_OPTIONS, &f)
 			return
-		} else if err := db.ChangeUserName(org, f.Name); err != nil {
+		} else if err := db.Users.ChangeUsername(c.Req.Context(), org.ID, f.Name); err != nil {
 			c.Data["OrgName"] = true
 			switch {
-			case db.IsErrNameNotAllowed(err):
+			case db.IsErrNameNotAllowed(errors.Cause(err)):
 				c.RenderWithErr(c.Tr("user.form.name_not_allowed", err.(db.ErrNameNotAllowed).Value()), SETTINGS_OPTIONS, &f)
 			default:
-				c.Error(err, "change user name")
+				c.Error(err, "change organization name")
 			}
 			return
 		}

+ 3 - 3
internal/route/user/setting.go

@@ -71,13 +71,13 @@ func SettingsPost(c *context.Context, f form.UpdateProfile) {
 	if c.User.IsLocal() {
 		// Check if username characters have been changed
 		if c.User.LowerName != strings.ToLower(f.Name) {
-			if err := db.ChangeUserName(c.User, f.Name); err != nil {
+			if err := db.Users.ChangeUsername(c.Req.Context(), c.User.ID, f.Name); err != nil {
 				c.FormErr("Name")
 				var msg string
 				switch {
-				case db.IsErrUserAlreadyExist(err):
+				case db.IsErrUserAlreadyExist(errors.Cause(err)):
 					msg = c.Tr("form.username_been_taken")
-				case db.IsErrNameNotAllowed(err):
+				case db.IsErrNameNotAllowed(errors.Cause(err)):
 					msg = c.Tr("user.form.name_not_allowed", err.(db.ErrNameNotAllowed).Value())
 				default:
 					c.Error(err, "change user name")