preserve users if restoring a repository on the same Gitea instance (#18604)

When calling DumpRepository and RestoreRepository on the same Gitea
instance, the users are preserved: all labels, issues etc. belong to
the external user who is, in this particular case, the local user.

Dead code verifying g.gitServiceType.Name() == "" (i.e. plain git) is
removed. The function is never called because the plain git downloader
does not migrate anything that is associated to a user, by definition.

Errors returned by GetUserIDByExternalUserID are no longer ignored.

The userMap is used when the external user is not kown, which is the
most common case. It was only used when the external user exists
which happens less often and, as a result, every occurence of an
unknown external user required a SQL query.

Signed-off-by: Loïc Dachary <loic@dachary.org>

Co-authored-by: Loïc Dachary <loic@dachary.org>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
singuliere 2022-02-06 10:05:29 +01:00 committed by GitHub
parent 9419dd2b62
commit 8bd89ca294
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 128 additions and 29 deletions

View file

@ -129,6 +129,7 @@ func TestDumpRestore(t *testing.T) {
assert.EqualValues(t, before[i].Created, after[i].Created) assert.EqualValues(t, before[i].Created, after[i].Created)
assert.EqualValues(t, before[i].Updated, after[i].Updated) assert.EqualValues(t, before[i].Updated, after[i].Updated)
assert.EqualValues(t, before[i].Labels, after[i].Labels) assert.EqualValues(t, before[i].Labels, after[i].Labels)
assert.EqualValues(t, before[i].Reactions, after[i].Reactions)
} }
} }
}) })

View file

@ -1025,6 +1025,19 @@ func GetUserNamesByIDs(ids []int64) ([]string, error) {
return unames, err return unames, err
} }
// GetUserNameByID returns username for the id
func GetUserNameByID(ctx context.Context, id int64) (string, error) {
var name string
has, err := db.GetEngine(db.DefaultContext).Table("user").Where("id = ?", id).Cols("name").Get(&name)
if err != nil {
return "", err
}
if has {
return name, nil
}
return "", nil
}
// GetUserIDsByNames returns a slice of ids corresponds to names. // GetUserIDsByNames returns a slice of ids corresponds to names.
func GetUserIDsByNames(names []string, ignoreNonExistent bool) ([]int64, error) { func GetUserIDsByNames(names []string, ignoreNonExistent bool) ([]int64, error) {
ids := make([]int64, 0, len(names)) ids := make([]int64, 0, len(names))

View file

@ -46,6 +46,7 @@ type GiteaLocalUploader struct {
issues map[int64]*models.Issue issues map[int64]*models.Issue
gitRepo *git.Repository gitRepo *git.Repository
prHeadCache map[string]struct{} prHeadCache map[string]struct{}
sameApp bool
userMap map[int64]int64 // external user id mapping to user id userMap map[int64]int64 // external user id mapping to user id
prCache map[int64]*models.PullRequest prCache map[int64]*models.PullRequest
gitServiceType structs.GitServiceType gitServiceType structs.GitServiceType
@ -128,6 +129,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate
MirrorInterval: opts.MirrorInterval, MirrorInterval: opts.MirrorInterval,
}, NewMigrationHTTPTransport()) }, NewMigrationHTTPTransport())
g.sameApp = strings.HasPrefix(repo.OriginalURL, setting.AppURL)
g.repo = r g.repo = r
if err != nil { if err != nil {
return err return err
@ -256,7 +258,7 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
CreatedUnix: timeutil.TimeStamp(release.Created.Unix()), CreatedUnix: timeutil.TimeStamp(release.Created.Unix()),
} }
if err := g.remapExternalUser(release, &rel); err != nil { if err := g.remapUser(release, &rel); err != nil {
return err return err
} }
@ -373,7 +375,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
UpdatedUnix: timeutil.TimeStamp(issue.Updated.Unix()), UpdatedUnix: timeutil.TimeStamp(issue.Updated.Unix()),
} }
if err := g.remapExternalUser(issue, &is); err != nil { if err := g.remapUser(issue, &is); err != nil {
return err return err
} }
@ -386,7 +388,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
Type: reaction.Content, Type: reaction.Content,
CreatedUnix: timeutil.TimeStampNow(), CreatedUnix: timeutil.TimeStampNow(),
} }
if err := g.remapExternalUser(reaction, &res); err != nil { if err := g.remapUser(reaction, &res); err != nil {
return err return err
} }
is.Reactions = append(is.Reactions, &res) is.Reactions = append(is.Reactions, &res)
@ -437,7 +439,7 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
UpdatedUnix: timeutil.TimeStamp(comment.Updated.Unix()), UpdatedUnix: timeutil.TimeStamp(comment.Updated.Unix()),
} }
if err := g.remapExternalUser(comment, &cm); err != nil { if err := g.remapUser(comment, &cm); err != nil {
return err return err
} }
@ -447,7 +449,7 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
Type: reaction.Content, Type: reaction.Content,
CreatedUnix: timeutil.TimeStampNow(), CreatedUnix: timeutil.TimeStampNow(),
} }
if err := g.remapExternalUser(reaction, &res); err != nil { if err := g.remapUser(reaction, &res); err != nil {
return err return err
} }
cm.Reactions = append(cm.Reactions, &res) cm.Reactions = append(cm.Reactions, &res)
@ -471,7 +473,7 @@ func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error
return err return err
} }
if err := g.remapExternalUser(pr, gpr.Issue); err != nil { if err := g.remapUser(pr, gpr.Issue); err != nil {
return err return err
} }
@ -626,7 +628,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR
UpdatedUnix: timeutil.TimeStamp(pr.Updated.Unix()), UpdatedUnix: timeutil.TimeStamp(pr.Updated.Unix()),
} }
if err := g.remapExternalUser(pr, &issue); err != nil { if err := g.remapUser(pr, &issue); err != nil {
return nil, err return nil, err
} }
@ -636,7 +638,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR
Type: reaction.Content, Type: reaction.Content,
CreatedUnix: timeutil.TimeStampNow(), CreatedUnix: timeutil.TimeStampNow(),
} }
if err := g.remapExternalUser(reaction, &res); err != nil { if err := g.remapUser(reaction, &res); err != nil {
return nil, err return nil, err
} }
issue.Reactions = append(issue.Reactions, &res) issue.Reactions = append(issue.Reactions, &res)
@ -710,7 +712,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
UpdatedUnix: timeutil.TimeStamp(review.CreatedAt.Unix()), UpdatedUnix: timeutil.TimeStamp(review.CreatedAt.Unix()),
} }
if err := g.remapExternalUser(review, &cm); err != nil { if err := g.remapUser(review, &cm); err != nil {
return err return err
} }
@ -773,7 +775,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
UpdatedUnix: timeutil.TimeStamp(comment.UpdatedAt.Unix()), UpdatedUnix: timeutil.TimeStamp(comment.UpdatedAt.Unix()),
} }
if err := g.remapExternalUser(review, &c); err != nil { if err := g.remapUser(review, &c); err != nil {
return err return err
} }
@ -816,23 +818,52 @@ func (g *GiteaLocalUploader) Finish() error {
return repo_model.UpdateRepositoryCols(g.repo, "status") return repo_model.UpdateRepositoryCols(g.repo, "status")
} }
func (g *GiteaLocalUploader) remapExternalUser(source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) (err error) { func (g *GiteaLocalUploader) remapUser(source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) error {
userid, ok := g.userMap[source.GetExternalID()] var userid int64
tp := g.gitServiceType.Name() var err error
if !ok && tp != "" { if g.sameApp {
userid, err = user_model.GetUserIDByExternalUserID(tp, fmt.Sprintf("%d", source.GetExternalID())) userid, err = g.remapLocalUser(source, target)
} else {
userid, err = g.remapExternalUser(source, target)
}
if err != nil { if err != nil {
log.Error("GetUserIDByExternalUserID: %v", err) return err
}
if userid > 0 {
g.userMap[source.GetExternalID()] = userid
}
} }
if userid > 0 { if userid > 0 {
err = target.RemapExternalUser("", 0, userid) return target.RemapExternalUser("", 0, userid)
}
return target.RemapExternalUser(source.GetExternalName(), source.GetExternalID(), g.doer.ID)
}
func (g *GiteaLocalUploader) remapLocalUser(source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) (int64, error) {
userid, ok := g.userMap[source.GetExternalID()]
if !ok {
name, err := user_model.GetUserNameByID(g.ctx, source.GetExternalID())
if err != nil {
return 0, err
}
// let's not reuse an ID when the user was deleted or has a different user name
if name != source.GetExternalName() {
userid = 0
} else { } else {
err = target.RemapExternalUser(source.GetExternalName(), source.GetExternalID(), g.doer.ID) userid = source.GetExternalID()
} }
return g.userMap[source.GetExternalID()] = userid
}
return userid, nil
}
func (g *GiteaLocalUploader) remapExternalUser(source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) (userid int64, err error) {
userid, ok := g.userMap[source.GetExternalID()]
if !ok {
userid, err = user_model.GetUserIDByExternalUserID(g.gitServiceType.Name(), fmt.Sprintf("%d", source.GetExternalID()))
if err != nil {
log.Error("GetUserIDByExternalUserID: %v", err)
return 0, err
}
g.userMap[source.GetExternalID()] = userid
}
return userid, nil
} }

View file

@ -117,6 +117,56 @@ func TestGiteaUploadRepo(t *testing.T) {
assert.Len(t, pulls[0].Issue.Comments, 2) assert.Len(t, pulls[0].Issue.Comments, 2)
} }
func TestGiteaUploadRemapLocalUser(t *testing.T) {
unittest.PrepareTestEnv(t)
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
repoName := "migrated"
uploader := NewGiteaLocalUploader(context.Background(), doer, doer.Name, repoName)
// call remapLocalUser
uploader.sameApp = true
externalID := int64(1234567)
externalName := "username"
source := base.Release{
PublisherID: externalID,
PublisherName: externalName,
}
//
// The externalID does not match any existing user, everything
// belongs to the doer
//
target := models.Release{}
uploader.userMap = make(map[int64]int64)
err := uploader.remapUser(&source, &target)
assert.NoError(t, err)
assert.EqualValues(t, doer.ID, target.GetUserID())
//
// The externalID matches a known user but the name does not match,
// everything belongs to the doer
//
source.PublisherID = user.ID
target = models.Release{}
uploader.userMap = make(map[int64]int64)
err = uploader.remapUser(&source, &target)
assert.NoError(t, err)
assert.EqualValues(t, doer.ID, target.GetUserID())
//
// The externalID and externalName match an existing user, everything
// belongs to the existing user
//
source.PublisherName = user.Name
target = models.Release{}
uploader.userMap = make(map[int64]int64)
err = uploader.remapUser(&source, &target)
assert.NoError(t, err)
assert.EqualValues(t, user.ID, target.GetUserID())
}
func TestGiteaUploadRemapExternalUser(t *testing.T) { func TestGiteaUploadRemapExternalUser(t *testing.T) {
unittest.PrepareTestEnv(t) unittest.PrepareTestEnv(t)
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
@ -124,9 +174,11 @@ func TestGiteaUploadRemapExternalUser(t *testing.T) {
repoName := "migrated" repoName := "migrated"
uploader := NewGiteaLocalUploader(context.Background(), doer, doer.Name, repoName) uploader := NewGiteaLocalUploader(context.Background(), doer, doer.Name, repoName)
uploader.gitServiceType = structs.GiteaService uploader.gitServiceType = structs.GiteaService
// call remapExternalUser
uploader.sameApp = false
externalID := int64(1234567) externalID := int64(1234567)
externalName := "url.or.something" externalName := "username"
source := base.Release{ source := base.Release{
PublisherID: externalID, PublisherID: externalID,
PublisherName: externalName, PublisherName: externalName,
@ -136,8 +188,9 @@ func TestGiteaUploadRemapExternalUser(t *testing.T) {
// When there is no user linked to the external ID, the migrated data is authored // When there is no user linked to the external ID, the migrated data is authored
// by the doer // by the doer
// //
uploader.userMap = make(map[int64]int64)
target := models.Release{} target := models.Release{}
err := uploader.remapExternalUser(&source, &target) err := uploader.remapUser(&source, &target)
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, doer.ID, target.GetUserID()) assert.EqualValues(t, doer.ID, target.GetUserID())
@ -158,8 +211,9 @@ func TestGiteaUploadRemapExternalUser(t *testing.T) {
// When a user is linked to the external ID, it becomes the author of // When a user is linked to the external ID, it becomes the author of
// the migrated data // the migrated data
// //
uploader.userMap = make(map[int64]int64)
target = models.Release{} target = models.Release{}
err = uploader.remapExternalUser(&source, &target) err = uploader.remapUser(&source, &target)
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, target.GetUserID(), linkedUser.ID) assert.EqualValues(t, linkedUser.ID, target.GetUserID())
} }