Some repository refactors (#17950)

* some repository refactors

* remove unnecessary code

* Fix test

* Remove unnecessary banner
This commit is contained in:
Lunny Xiao
2021-12-12 23:48:20 +08:00
committed by GitHub
parent 0a7e8327a0
commit 5723240490
88 changed files with 1363 additions and 1388 deletions

View File

@ -107,3 +107,10 @@ func FindRepoArchives(opts FindRepoArchiversOption) ([]*RepoArchiver, error) {
Find(&archivers)
return archivers, err
}
// SetArchiveRepoState sets if a repo is archived
func SetArchiveRepoState(repo *Repository, isArchived bool) (err error) {
repo.IsArchived = isArchived
_, err = db.GetEngine(db.DefaultContext).Where("id = ?", repo.ID).Cols("is_archived").NoAutoTime().Update(repo)
return
}

69
models/repo/fork.go Normal file
View File

@ -0,0 +1,69 @@
// Copyright 2021 The Gitea 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 repo
import (
"context"
"code.gitea.io/gitea/models/db"
)
func getRepositoriesByForkID(e db.Engine, forkID int64) ([]*Repository, error) {
repos := make([]*Repository, 0, 10)
return repos, e.
Where("fork_id=?", forkID).
Find(&repos)
}
// GetRepositoriesByForkID returns all repositories with given fork ID.
func GetRepositoriesByForkID(ctx context.Context, forkID int64) ([]*Repository, error) {
return getRepositoriesByForkID(db.GetEngine(ctx), forkID)
}
// GetForkedRepo checks if given user has already forked a repository with given ID.
func GetForkedRepo(ownerID, repoID int64) *Repository {
repo := new(Repository)
has, _ := db.GetEngine(db.DefaultContext).
Where("owner_id=? AND fork_id=?", ownerID, repoID).
Get(repo)
if has {
return repo
}
return nil
}
// HasForkedRepo checks if given user has already forked a repository with given ID.
func HasForkedRepo(ownerID, repoID int64) bool {
has, _ := db.GetEngine(db.DefaultContext).
Table("repository").
Where("owner_id=? AND fork_id=?", ownerID, repoID).
Exist()
return has
}
// GetUserFork return user forked repository from this repository, if not forked return nil
func GetUserFork(repoID, userID int64) (*Repository, error) {
var forkedRepo Repository
has, err := db.GetEngine(db.DefaultContext).Where("fork_id = ?", repoID).And("owner_id = ?", userID).Get(&forkedRepo)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return &forkedRepo, nil
}
// GetForks returns all the forks of the repository
func GetForks(repo *Repository, listOptions db.ListOptions) ([]*Repository, error) {
if listOptions.Page == 0 {
forks := make([]*Repository, 0, repo.NumForks)
return forks, db.GetEngine(db.DefaultContext).Find(&forks, &Repository{ForkID: repo.ID})
}
sess := db.GetPaginatedSession(&listOptions)
forks := make([]*Repository, 0, listOptions.PageSize)
return forks, sess.Find(&forks, &Repository{ForkID: repo.ID})
}

32
models/repo/fork_test.go Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2021 The Gitea 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 repo
import (
"testing"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestGetUserFork(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
// User13 has repo 11 forked from repo10
repo, err := GetRepositoryByID(10)
assert.NoError(t, err)
assert.NotNil(t, repo)
repo, err = GetUserFork(repo.ID, 13)
assert.NoError(t, err)
assert.NotNil(t, repo)
repo, err = GetRepositoryByID(9)
assert.NoError(t, err)
assert.NotNil(t, repo)
repo, err = GetUserFork(repo.ID, 13)
assert.NoError(t, err)
assert.Nil(t, repo)
}

View File

@ -18,5 +18,11 @@ func TestMain(m *testing.M) {
"repository.yml",
"repo_unit.yml",
"repo_indexer_status.yml",
"repo_redirect.yml",
"watch.yml",
"star.yml",
"topic.yml",
"repo_topic.yml",
"user.yml",
)
}

82
models/repo/redirect.go Normal file
View File

@ -0,0 +1,82 @@
// Copyright 2017 The Gitea 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 repo
import (
"context"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
)
// ErrRedirectNotExist represents a "RedirectNotExist" kind of error.
type ErrRedirectNotExist struct {
OwnerID int64
RepoName string
}
// IsErrRedirectNotExist check if an error is an ErrRepoRedirectNotExist.
func IsErrRedirectNotExist(err error) bool {
_, ok := err.(ErrRedirectNotExist)
return ok
}
func (err ErrRedirectNotExist) Error() string {
return fmt.Sprintf("repository redirect does not exist [uid: %d, name: %s]", err.OwnerID, err.RepoName)
}
// Redirect represents that a repo name should be redirected to another
type Redirect struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"UNIQUE(s)"`
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"`
RedirectRepoID int64 // repoID to redirect to
}
// TableName represents real table name in database
func (Redirect) TableName() string {
return "repo_redirect"
}
func init() {
db.RegisterModel(new(Redirect))
}
// LookupRedirect look up if a repository has a redirect name
func LookupRedirect(ownerID int64, repoName string) (int64, error) {
repoName = strings.ToLower(repoName)
redirect := &Redirect{OwnerID: ownerID, LowerName: repoName}
if has, err := db.GetEngine(db.DefaultContext).Get(redirect); err != nil {
return 0, err
} else if !has {
return 0, ErrRedirectNotExist{OwnerID: ownerID, RepoName: repoName}
}
return redirect.RedirectRepoID, nil
}
// NewRedirect create a new repo redirect
func NewRedirect(ctx context.Context, ownerID, repoID int64, oldRepoName, newRepoName string) error {
oldRepoName = strings.ToLower(oldRepoName)
newRepoName = strings.ToLower(newRepoName)
if err := DeleteRedirect(ctx, ownerID, newRepoName); err != nil {
return err
}
return db.Insert(ctx, &Redirect{
OwnerID: ownerID,
LowerName: oldRepoName,
RedirectRepoID: repoID,
})
}
// DeleteRedirect delete any redirect from the specified repo name to
// anything else
func DeleteRedirect(ctx context.Context, ownerID int64, repoName string) error {
repoName = strings.ToLower(repoName)
_, err := db.GetEngine(ctx).Delete(&Redirect{OwnerID: ownerID, LowerName: repoName})
return err
}

View File

@ -0,0 +1,77 @@
// Copyright 2017 The Gitea 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 repo
import (
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestLookupRedirect(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repoID, err := LookupRedirect(2, "oldrepo1")
assert.NoError(t, err)
assert.EqualValues(t, 1, repoID)
_, err = LookupRedirect(unittest.NonexistentID, "doesnotexist")
assert.True(t, IsErrRedirectNotExist(err))
}
func TestNewRedirect(t *testing.T) {
// redirect to a completely new name
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
assert.NoError(t, NewRedirect(db.DefaultContext, repo.OwnerID, repo.ID, repo.Name, "newreponame"))
unittest.AssertExistsAndLoadBean(t, &Redirect{
OwnerID: repo.OwnerID,
LowerName: repo.LowerName,
RedirectRepoID: repo.ID,
})
unittest.AssertExistsAndLoadBean(t, &Redirect{
OwnerID: repo.OwnerID,
LowerName: "oldrepo1",
RedirectRepoID: repo.ID,
})
}
func TestNewRedirect2(t *testing.T) {
// redirect to previously used name
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
assert.NoError(t, NewRedirect(db.DefaultContext, repo.OwnerID, repo.ID, repo.Name, "oldrepo1"))
unittest.AssertExistsAndLoadBean(t, &Redirect{
OwnerID: repo.OwnerID,
LowerName: repo.LowerName,
RedirectRepoID: repo.ID,
})
unittest.AssertNotExistsBean(t, &Redirect{
OwnerID: repo.OwnerID,
LowerName: "oldrepo1",
RedirectRepoID: repo.ID,
})
}
func TestNewRedirect3(t *testing.T) {
// redirect for a previously-unredirected repo
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
assert.NoError(t, NewRedirect(db.DefaultContext, repo.OwnerID, repo.ID, repo.Name, "newreponame"))
unittest.AssertExistsAndLoadBean(t, &Redirect{
OwnerID: repo.OwnerID,
LowerName: repo.LowerName,
RedirectRepoID: repo.ID,
})
}

View File

@ -25,6 +25,20 @@ import (
"code.gitea.io/gitea/modules/util"
)
var (
reservedRepoNames = []string{".", ".."}
reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"}
)
// IsUsableRepoName returns true when repository is usable
func IsUsableRepoName(name string) error {
if db.AlphaDashDotPattern.MatchString(name) {
// Note: usually this error is normally caught up earlier in the UI
return db.ErrNameCharsNotAllowed{Name: name}
}
return db.IsUsableName(reservedRepoNames, reservedRepoPatterns, name)
}
// TrustModelType defines the types of trust model for this repository
type TrustModelType int
@ -734,3 +748,25 @@ func GetPublicRepositoryCount(u *user_model.User) (int64, error) {
func GetPrivateRepositoryCount(u *user_model.User) (int64, error) {
return getPrivateRepositoryCount(db.GetEngine(db.DefaultContext), u)
}
// IterateRepository iterate repositories
func IterateRepository(f func(repo *Repository) error) error {
var start int
batchSize := setting.Database.IterateBufferSize
for {
repos := make([]*Repository, 0, batchSize)
if err := db.GetEngine(db.DefaultContext).Limit(batchSize, start).Find(&repos); err != nil {
return err
}
if len(repos) == 0 {
return nil
}
start += len(repos)
for _, repo := range repos {
if err := f(repo); err != nil {
return err
}
}
}
}

View File

@ -42,3 +42,10 @@ func TestGetPrivateRepositoryCount(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, int64(2), count)
}
func TestRepoAPIURL(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository)
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user12/repo10", repo.APIURL())
}

View File

@ -242,3 +242,29 @@ func UpdateRepoUnit(unit *RepoUnit) error {
_, err := db.GetEngine(db.DefaultContext).ID(unit.ID).Update(unit)
return err
}
// UpdateRepositoryUnits updates a repository's units
func UpdateRepositoryUnits(repo *Repository, units []RepoUnit, deleteUnitTypes []unit.Type) (err error) {
ctx, committer, err := db.TxContext()
if err != nil {
return err
}
defer committer.Close()
// Delete existing settings of units before adding again
for _, u := range units {
deleteUnitTypes = append(deleteUnitTypes, u.Type)
}
if _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).In("type", deleteUnitTypes).Delete(new(RepoUnit)); err != nil {
return err
}
if len(units) > 0 {
if err = db.Insert(ctx, units); err != nil {
return err
}
}
return committer.Commit()
}

90
models/repo/star.go Normal file
View File

@ -0,0 +1,90 @@
// Copyright 2016 The Gitea 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 repo
import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil"
)
// Star represents a starred repo by an user.
type Star struct {
ID int64 `xorm:"pk autoincr"`
UID int64 `xorm:"UNIQUE(s)"`
RepoID int64 `xorm:"UNIQUE(s)"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
}
func init() {
db.RegisterModel(new(Star))
}
// StarRepo or unstar repository.
func StarRepo(userID, repoID int64, star bool) error {
ctx, committer, err := db.TxContext()
if err != nil {
return err
}
defer committer.Close()
staring := isStaring(db.GetEngine(ctx), userID, repoID)
if star {
if staring {
return nil
}
if err := db.Insert(ctx, &Star{UID: userID, RepoID: repoID}); err != nil {
return err
}
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repoID); err != nil {
return err
}
if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", userID); err != nil {
return err
}
} else {
if !staring {
return nil
}
if _, err := db.DeleteByBean(ctx, &Star{UID: userID, RepoID: repoID}); err != nil {
return err
}
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repoID); err != nil {
return err
}
if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", userID); err != nil {
return err
}
}
return committer.Commit()
}
// IsStaring checks if user has starred given repository.
func IsStaring(userID, repoID int64) bool {
return isStaring(db.GetEngine(db.DefaultContext), userID, repoID)
}
func isStaring(e db.Engine, userID, repoID int64) bool {
has, _ := e.Get(&Star{UID: userID, RepoID: repoID})
return has
}
// GetStargazers returns the users that starred the repo.
func GetStargazers(repo *Repository, opts db.ListOptions) ([]*user_model.User, error) {
sess := db.GetEngine(db.DefaultContext).Where("star.repo_id = ?", repo.ID).
Join("LEFT", "star", "`user`.id = star.uid")
if opts.Page > 0 {
sess = db.SetSessionPagination(sess, &opts)
users := make([]*user_model.User, 0, opts.PageSize)
return users, sess.Find(&users)
}
users := make([]*user_model.User, 0, 8)
return users, sess.Find(&users)
}

53
models/repo/star_test.go Normal file
View File

@ -0,0 +1,53 @@
// Copyright 2017 The Gitea 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 repo
import (
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestStarRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const userID = 2
const repoID = 1
unittest.AssertNotExistsBean(t, &Star{UID: userID, RepoID: repoID})
assert.NoError(t, StarRepo(userID, repoID, true))
unittest.AssertExistsAndLoadBean(t, &Star{UID: userID, RepoID: repoID})
assert.NoError(t, StarRepo(userID, repoID, true))
unittest.AssertExistsAndLoadBean(t, &Star{UID: userID, RepoID: repoID})
assert.NoError(t, StarRepo(userID, repoID, false))
unittest.AssertNotExistsBean(t, &Star{UID: userID, RepoID: repoID})
}
func TestIsStaring(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
assert.True(t, IsStaring(2, 4))
assert.False(t, IsStaring(3, 4))
}
func TestRepository_GetStargazers(t *testing.T) {
// repo with stargazers
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 4}).(*Repository)
gazers, err := GetStargazers(repo, db.ListOptions{Page: 0})
assert.NoError(t, err)
if assert.Len(t, gazers, 1) {
assert.Equal(t, int64(2), gazers[0].ID)
}
}
func TestRepository_GetStargazers2(t *testing.T) {
// repo with stargazers
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository)
gazers, err := GetStargazers(repo, db.ListOptions{Page: 0})
assert.NoError(t, err)
assert.Len(t, gazers, 0)
}

369
models/repo/topic.go Normal file
View File

@ -0,0 +1,369 @@
// Copyright 2018 The Gitea 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 repo
import (
"context"
"fmt"
"regexp"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/builder"
)
func init() {
db.RegisterModel(new(Topic))
db.RegisterModel(new(RepoTopic))
}
var topicPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
// Topic represents a topic of repositories
type Topic struct {
ID int64 `xorm:"pk autoincr"`
Name string `xorm:"UNIQUE VARCHAR(50)"`
RepoCount int
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
// RepoTopic represents associated repositories and topics
type RepoTopic struct { //revive:disable-line:exported
RepoID int64 `xorm:"pk"`
TopicID int64 `xorm:"pk"`
}
// ErrTopicNotExist represents an error that a topic is not exist
type ErrTopicNotExist struct {
Name string
}
// IsErrTopicNotExist checks if an error is an ErrTopicNotExist.
func IsErrTopicNotExist(err error) bool {
_, ok := err.(ErrTopicNotExist)
return ok
}
// Error implements error interface
func (err ErrTopicNotExist) Error() string {
return fmt.Sprintf("topic is not exist [name: %s]", err.Name)
}
// ValidateTopic checks a topic by length and match pattern rules
func ValidateTopic(topic string) bool {
return len(topic) <= 35 && topicPattern.MatchString(topic)
}
// SanitizeAndValidateTopics sanitizes and checks an array or topics
func SanitizeAndValidateTopics(topics []string) (validTopics, invalidTopics []string) {
validTopics = make([]string, 0)
mValidTopics := make(map[string]struct{})
invalidTopics = make([]string, 0)
for _, topic := range topics {
topic = strings.TrimSpace(strings.ToLower(topic))
// ignore empty string
if len(topic) == 0 {
continue
}
// ignore same topic twice
if _, ok := mValidTopics[topic]; ok {
continue
}
if ValidateTopic(topic) {
validTopics = append(validTopics, topic)
mValidTopics[topic] = struct{}{}
} else {
invalidTopics = append(invalidTopics, topic)
}
}
return validTopics, invalidTopics
}
// GetTopicByName retrieves topic by name
func GetTopicByName(name string) (*Topic, error) {
var topic Topic
if has, err := db.GetEngine(db.DefaultContext).Where("name = ?", name).Get(&topic); err != nil {
return nil, err
} else if !has {
return nil, ErrTopicNotExist{name}
}
return &topic, nil
}
// addTopicByNameToRepo adds a topic name to a repo and increments the topic count.
// Returns topic after the addition
func addTopicByNameToRepo(e db.Engine, repoID int64, topicName string) (*Topic, error) {
var topic Topic
has, err := e.Where("name = ?", topicName).Get(&topic)
if err != nil {
return nil, err
}
if !has {
topic.Name = topicName
topic.RepoCount = 1
if _, err := e.Insert(&topic); err != nil {
return nil, err
}
} else {
topic.RepoCount++
if _, err := e.ID(topic.ID).Cols("repo_count").Update(&topic); err != nil {
return nil, err
}
}
if _, err := e.Insert(&RepoTopic{
RepoID: repoID,
TopicID: topic.ID,
}); err != nil {
return nil, err
}
return &topic, nil
}
// removeTopicFromRepo remove a topic from a repo and decrements the topic repo count
func removeTopicFromRepo(e db.Engine, repoID int64, topic *Topic) error {
topic.RepoCount--
if _, err := e.ID(topic.ID).Cols("repo_count").Update(topic); err != nil {
return err
}
if _, err := e.Delete(&RepoTopic{
RepoID: repoID,
TopicID: topic.ID,
}); err != nil {
return err
}
return nil
}
// RemoveTopicsFromRepo remove all topics from the repo and decrements respective topics repo count
func RemoveTopicsFromRepo(ctx context.Context, repoID int64) error {
e := db.GetEngine(ctx)
_, err := e.Where(
builder.In("id",
builder.Select("topic_id").From("repo_topic").Where(builder.Eq{"repo_id": repoID}),
),
).Cols("repo_count").SetExpr("repo_count", "repo_count-1").Update(&Topic{})
if err != nil {
return err
}
if _, err = e.Delete(&RepoTopic{RepoID: repoID}); err != nil {
return err
}
return nil
}
// FindTopicOptions represents the options when fdin topics
type FindTopicOptions struct {
db.ListOptions
RepoID int64
Keyword string
}
func (opts *FindTopicOptions) toConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_topic.repo_id": opts.RepoID})
}
if opts.Keyword != "" {
cond = cond.And(builder.Like{"topic.name", opts.Keyword})
}
return cond
}
// FindTopics retrieves the topics via FindTopicOptions
func FindTopics(opts *FindTopicOptions) ([]*Topic, int64, error) {
sess := db.GetEngine(db.DefaultContext).Select("topic.*").Where(opts.toConds())
if opts.RepoID > 0 {
sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
}
if opts.PageSize != 0 && opts.Page != 0 {
sess = db.SetSessionPagination(sess, opts)
}
topics := make([]*Topic, 0, 10)
total, err := sess.Desc("topic.repo_count").FindAndCount(&topics)
return topics, total, err
}
// CountTopics counts the number of topics matching the FindTopicOptions
func CountTopics(opts *FindTopicOptions) (int64, error) {
sess := db.GetEngine(db.DefaultContext).Where(opts.toConds())
if opts.RepoID > 0 {
sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
}
return sess.Count(new(Topic))
}
// GetRepoTopicByName retrieves topic from name for a repo if it exist
func GetRepoTopicByName(repoID int64, topicName string) (*Topic, error) {
return getRepoTopicByName(db.GetEngine(db.DefaultContext), repoID, topicName)
}
func getRepoTopicByName(e db.Engine, repoID int64, topicName string) (*Topic, error) {
cond := builder.NewCond()
var topic Topic
cond = cond.And(builder.Eq{"repo_topic.repo_id": repoID}).And(builder.Eq{"topic.name": topicName})
sess := e.Table("topic").Where(cond)
sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
has, err := sess.Get(&topic)
if has {
return &topic, err
}
return nil, err
}
// AddTopic adds a topic name to a repository (if it does not already have it)
func AddTopic(repoID int64, topicName string) (*Topic, error) {
ctx, committer, err := db.TxContext()
if err != nil {
return nil, err
}
defer committer.Close()
sess := db.GetEngine(ctx)
topic, err := getRepoTopicByName(sess, repoID, topicName)
if err != nil {
return nil, err
}
if topic != nil {
// Repo already have topic
return topic, nil
}
topic, err = addTopicByNameToRepo(sess, repoID, topicName)
if err != nil {
return nil, err
}
topicNames := make([]string, 0, 25)
if err := sess.Select("name").Table("topic").
Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id").
Where("repo_topic.repo_id = ?", repoID).Desc("topic.repo_count").Find(&topicNames); err != nil {
return nil, err
}
if _, err := sess.ID(repoID).Cols("topics").Update(&Repository{
Topics: topicNames,
}); err != nil {
return nil, err
}
return topic, committer.Commit()
}
// DeleteTopic removes a topic name from a repository (if it has it)
func DeleteTopic(repoID int64, topicName string) (*Topic, error) {
topic, err := GetRepoTopicByName(repoID, topicName)
if err != nil {
return nil, err
}
if topic == nil {
// Repo doesn't have topic, can't be removed
return nil, nil
}
err = removeTopicFromRepo(db.GetEngine(db.DefaultContext), repoID, topic)
return topic, err
}
// SaveTopics save topics to a repository
func SaveTopics(repoID int64, topicNames ...string) error {
topics, _, err := FindTopics(&FindTopicOptions{
RepoID: repoID,
})
if err != nil {
return err
}
ctx, committer, err := db.TxContext()
if err != nil {
return err
}
defer committer.Close()
sess := db.GetEngine(ctx)
var addedTopicNames []string
for _, topicName := range topicNames {
if strings.TrimSpace(topicName) == "" {
continue
}
var found bool
for _, t := range topics {
if strings.EqualFold(topicName, t.Name) {
found = true
break
}
}
if !found {
addedTopicNames = append(addedTopicNames, topicName)
}
}
var removeTopics []*Topic
for _, t := range topics {
var found bool
for _, topicName := range topicNames {
if strings.EqualFold(topicName, t.Name) {
found = true
break
}
}
if !found {
removeTopics = append(removeTopics, t)
}
}
for _, topicName := range addedTopicNames {
_, err := addTopicByNameToRepo(sess, repoID, topicName)
if err != nil {
return err
}
}
for _, topic := range removeTopics {
err := removeTopicFromRepo(sess, repoID, topic)
if err != nil {
return err
}
}
topicNames = make([]string, 0, 25)
if err := sess.Table("topic").Cols("name").
Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id").
Where("repo_topic.repo_id = ?", repoID).Desc("topic.repo_count").Find(&topicNames); err != nil {
return err
}
if _, err := sess.ID(repoID).Cols("topics").Update(&Repository{
Topics: topicNames,
}); err != nil {
return err
}
return committer.Commit()
}
// GenerateTopics generates topics from a template repository
func GenerateTopics(ctx context.Context, templateRepo, generateRepo *Repository) error {
for _, topic := range templateRepo.Topics {
if _, err := addTopicByNameToRepo(db.GetEngine(ctx), generateRepo.ID, topic); err != nil {
return err
}
}
return nil
}

80
models/repo/topic_test.go Normal file
View File

@ -0,0 +1,80 @@
// Copyright 2018 The Gitea 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 repo
import (
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestAddTopic(t *testing.T) {
totalNrOfTopics := 6
repo1NrOfTopics := 3
assert.NoError(t, unittest.PrepareTestDatabase())
topics, _, err := FindTopics(&FindTopicOptions{})
assert.NoError(t, err)
assert.Len(t, topics, totalNrOfTopics)
topics, total, err := FindTopics(&FindTopicOptions{
ListOptions: db.ListOptions{Page: 1, PageSize: 2},
})
assert.NoError(t, err)
assert.Len(t, topics, 2)
assert.EqualValues(t, 6, total)
topics, _, err = FindTopics(&FindTopicOptions{
RepoID: 1,
})
assert.NoError(t, err)
assert.Len(t, topics, repo1NrOfTopics)
assert.NoError(t, SaveTopics(2, "golang"))
repo2NrOfTopics := 1
topics, _, err = FindTopics(&FindTopicOptions{})
assert.NoError(t, err)
assert.Len(t, topics, totalNrOfTopics)
topics, _, err = FindTopics(&FindTopicOptions{
RepoID: 2,
})
assert.NoError(t, err)
assert.Len(t, topics, repo2NrOfTopics)
assert.NoError(t, SaveTopics(2, "golang", "gitea"))
repo2NrOfTopics = 2
totalNrOfTopics++
topic, err := GetTopicByName("gitea")
assert.NoError(t, err)
assert.EqualValues(t, 1, topic.RepoCount)
topics, _, err = FindTopics(&FindTopicOptions{})
assert.NoError(t, err)
assert.Len(t, topics, totalNrOfTopics)
topics, _, err = FindTopics(&FindTopicOptions{
RepoID: 2,
})
assert.NoError(t, err)
assert.Len(t, topics, repo2NrOfTopics)
}
func TestTopicValidator(t *testing.T) {
assert.True(t, ValidateTopic("12345"))
assert.True(t, ValidateTopic("2-test"))
assert.True(t, ValidateTopic("test-3"))
assert.True(t, ValidateTopic("first"))
assert.True(t, ValidateTopic("second-test-topic"))
assert.True(t, ValidateTopic("third-project-topic-with-max-length"))
assert.False(t, ValidateTopic("$fourth-test,topic"))
assert.False(t, ValidateTopic("-fifth-test-topic"))
assert.False(t, ValidateTopic("sixth-go-project-topic-with-excess-length"))
}

179
models/repo/update.go Normal file
View File

@ -0,0 +1,179 @@
// Copyright 2021 The Gitea 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 repo
import (
"context"
"fmt"
"strings"
"time"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
// UpdateRepositoryOwnerNames updates repository owner_names (this should only be used when the ownerName has changed case)
func UpdateRepositoryOwnerNames(ownerID int64, ownerName string) error {
if ownerID == 0 {
return nil
}
ctx, committer, err := db.TxContext()
if err != nil {
return err
}
defer committer.Close()
if _, err := db.GetEngine(ctx).Where("owner_id = ?", ownerID).Cols("owner_name").Update(&Repository{
OwnerName: ownerName,
}); err != nil {
return err
}
return committer.Commit()
}
// UpdateRepositoryUpdatedTime updates a repository's updated time
func UpdateRepositoryUpdatedTime(repoID int64, updateTime time.Time) error {
_, err := db.GetEngine(db.DefaultContext).Exec("UPDATE repository SET updated_unix = ? WHERE id = ?", updateTime.Unix(), repoID)
return err
}
// UpdateRepositoryColsCtx updates repository's columns
func UpdateRepositoryColsCtx(ctx context.Context, repo *Repository, cols ...string) error {
_, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).Update(repo)
return err
}
// UpdateRepositoryCols updates repository's columns
func UpdateRepositoryCols(repo *Repository, cols ...string) error {
return UpdateRepositoryColsCtx(db.DefaultContext, repo, cols...)
}
// ErrReachLimitOfRepo represents a "ReachLimitOfRepo" kind of error.
type ErrReachLimitOfRepo struct {
Limit int
}
// IsErrReachLimitOfRepo checks if an error is a ErrReachLimitOfRepo.
func IsErrReachLimitOfRepo(err error) bool {
_, ok := err.(ErrReachLimitOfRepo)
return ok
}
func (err ErrReachLimitOfRepo) Error() string {
return fmt.Sprintf("user has reached maximum limit of repositories [limit: %d]", err.Limit)
}
// ErrRepoAlreadyExist represents a "RepoAlreadyExist" kind of error.
type ErrRepoAlreadyExist struct {
Uname string
Name string
}
// IsErrRepoAlreadyExist checks if an error is a ErrRepoAlreadyExist.
func IsErrRepoAlreadyExist(err error) bool {
_, ok := err.(ErrRepoAlreadyExist)
return ok
}
func (err ErrRepoAlreadyExist) Error() string {
return fmt.Sprintf("repository already exists [uname: %s, name: %s]", err.Uname, err.Name)
}
// ErrRepoFilesAlreadyExist represents a "RepoFilesAlreadyExist" kind of error.
type ErrRepoFilesAlreadyExist struct {
Uname string
Name string
}
// IsErrRepoFilesAlreadyExist checks if an error is a ErrRepoAlreadyExist.
func IsErrRepoFilesAlreadyExist(err error) bool {
_, ok := err.(ErrRepoFilesAlreadyExist)
return ok
}
func (err ErrRepoFilesAlreadyExist) Error() string {
return fmt.Sprintf("repository files already exist [uname: %s, name: %s]", err.Uname, err.Name)
}
// CheckCreateRepository check if could created a repository
func CheckCreateRepository(doer, u *user_model.User, name string, overwriteOrAdopt bool) error {
if !doer.CanCreateRepo() {
return ErrReachLimitOfRepo{u.MaxRepoCreation}
}
if err := IsUsableRepoName(name); err != nil {
return err
}
has, err := IsRepositoryExist(u, name)
if err != nil {
return fmt.Errorf("IsRepositoryExist: %v", err)
} else if has {
return ErrRepoAlreadyExist{u.Name, name}
}
repoPath := RepoPath(u.Name, name)
isExist, err := util.IsExist(repoPath)
if err != nil {
log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
return err
}
if !overwriteOrAdopt && isExist {
return ErrRepoFilesAlreadyExist{u.Name, name}
}
return nil
}
// ChangeRepositoryName changes all corresponding setting from old repository name to new one.
func ChangeRepositoryName(doer *user_model.User, repo *Repository, newRepoName string) (err error) {
oldRepoName := repo.Name
newRepoName = strings.ToLower(newRepoName)
if err = IsUsableRepoName(newRepoName); err != nil {
return err
}
if err := repo.GetOwner(db.DefaultContext); err != nil {
return err
}
has, err := IsRepositoryExist(repo.Owner, newRepoName)
if err != nil {
return fmt.Errorf("IsRepositoryExist: %v", err)
} else if has {
return ErrRepoAlreadyExist{repo.Owner.Name, newRepoName}
}
newRepoPath := RepoPath(repo.Owner.Name, newRepoName)
if err = util.Rename(repo.RepoPath(), newRepoPath); err != nil {
return fmt.Errorf("rename repository directory: %v", err)
}
wikiPath := repo.WikiPath()
isExist, err := util.IsExist(wikiPath)
if err != nil {
log.Error("Unable to check if %s exists. Error: %v", wikiPath, err)
return err
}
if isExist {
if err = util.Rename(wikiPath, WikiPath(repo.Owner.Name, newRepoName)); err != nil {
return fmt.Errorf("rename repository wiki: %v", err)
}
}
ctx, committer, err := db.TxContext()
if err != nil {
return err
}
defer committer.Close()
if err := NewRedirect(ctx, repo.Owner.ID, repo.ID, oldRepoName, newRepoName); err != nil {
return err
}
return committer.Commit()
}

196
models/repo/watch.go Normal file
View File

@ -0,0 +1,196 @@
// Copyright 2017 The Gitea 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 repo
import (
"context"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
)
// WatchMode specifies what kind of watch the user has on a repository
type WatchMode int8
const (
// WatchModeNone don't watch
WatchModeNone WatchMode = iota // 0
// WatchModeNormal watch repository (from other sources)
WatchModeNormal // 1
// WatchModeDont explicit don't auto-watch
WatchModeDont // 2
// WatchModeAuto watch repository (from AutoWatchOnChanges)
WatchModeAuto // 3
)
// Watch is connection request for receiving repository notification.
type Watch struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"UNIQUE(watch)"`
RepoID int64 `xorm:"UNIQUE(watch)"`
Mode WatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
func init() {
db.RegisterModel(new(Watch))
}
// GetWatch gets what kind of subscription a user has on a given repository; returns dummy record if none found
func GetWatch(ctx context.Context, userID, repoID int64) (Watch, error) {
watch := Watch{UserID: userID, RepoID: repoID}
has, err := db.GetEngine(ctx).Get(&watch)
if err != nil {
return watch, err
}
if !has {
watch.Mode = WatchModeNone
}
return watch, nil
}
// IsWatchMode Decodes watchability of WatchMode
func IsWatchMode(mode WatchMode) bool {
return mode != WatchModeNone && mode != WatchModeDont
}
// IsWatching checks if user has watched given repository.
func IsWatching(userID, repoID int64) bool {
watch, err := GetWatch(db.DefaultContext, userID, repoID)
return err == nil && IsWatchMode(watch.Mode)
}
func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error) {
if watch.Mode == mode {
return nil
}
if mode == WatchModeAuto && (watch.Mode == WatchModeDont || IsWatchMode(watch.Mode)) {
// Don't auto watch if already watching or deliberately not watching
return nil
}
hadrec := watch.Mode != WatchModeNone
needsrec := mode != WatchModeNone
repodiff := 0
if IsWatchMode(mode) && !IsWatchMode(watch.Mode) {
repodiff = 1
} else if !IsWatchMode(mode) && IsWatchMode(watch.Mode) {
repodiff = -1
}
watch.Mode = mode
e := db.GetEngine(ctx)
if !hadrec && needsrec {
watch.Mode = mode
if _, err = e.Insert(watch); err != nil {
return err
}
} else if needsrec {
watch.Mode = mode
if _, err := e.ID(watch.ID).AllCols().Update(watch); err != nil {
return err
}
} else if _, err = e.Delete(Watch{ID: watch.ID}); err != nil {
return err
}
if repodiff != 0 {
_, err = e.Exec("UPDATE `repository` SET num_watches = num_watches + ? WHERE id = ?", repodiff, watch.RepoID)
}
return err
}
// WatchRepoMode watch repository in specific mode.
func WatchRepoMode(userID, repoID int64, mode WatchMode) (err error) {
var watch Watch
if watch, err = GetWatch(db.DefaultContext, userID, repoID); err != nil {
return err
}
return watchRepoMode(db.DefaultContext, watch, mode)
}
// WatchRepoCtx watch or unwatch repository.
func WatchRepoCtx(ctx context.Context, userID, repoID int64, doWatch bool) (err error) {
var watch Watch
if watch, err = GetWatch(ctx, userID, repoID); err != nil {
return err
}
if !doWatch && watch.Mode == WatchModeAuto {
err = watchRepoMode(ctx, watch, WatchModeDont)
} else if !doWatch {
err = watchRepoMode(ctx, watch, WatchModeNone)
} else {
err = watchRepoMode(ctx, watch, WatchModeNormal)
}
return err
}
// WatchRepo watch or unwatch repository.
func WatchRepo(userID, repoID int64, watch bool) (err error) {
return WatchRepoCtx(db.DefaultContext, userID, repoID, watch)
}
// GetWatchers returns all watchers of given repository.
func GetWatchers(ctx context.Context, repoID int64) ([]*Watch, error) {
watches := make([]*Watch, 0, 10)
return watches, db.GetEngine(ctx).Where("`watch`.repo_id=?", repoID).
And("`watch`.mode<>?", WatchModeDont).
And("`user`.is_active=?", true).
And("`user`.prohibit_login=?", false).
Join("INNER", "`user`", "`user`.id = `watch`.user_id").
Find(&watches)
}
// GetRepoWatchersIDs returns IDs of watchers for a given repo ID
// but avoids joining with `user` for performance reasons
// User permissions must be verified elsewhere if required
func GetRepoWatchersIDs(ctx context.Context, repoID int64) ([]int64, error) {
ids := make([]int64, 0, 64)
return ids, db.GetEngine(ctx).Table("watch").
Where("watch.repo_id=?", repoID).
And("watch.mode<>?", WatchModeDont).
Select("user_id").
Find(&ids)
}
// GetRepoWatchers returns range of users watching given repository.
func GetRepoWatchers(repoID int64, opts db.ListOptions) ([]*user_model.User, error) {
sess := db.GetEngine(db.DefaultContext).Where("watch.repo_id=?", repoID).
Join("LEFT", "watch", "`user`.id=`watch`.user_id").
And("`watch`.mode<>?", WatchModeDont)
if opts.Page > 0 {
sess = db.SetSessionPagination(sess, &opts)
users := make([]*user_model.User, 0, opts.PageSize)
return users, sess.Find(&users)
}
users := make([]*user_model.User, 0, 8)
return users, sess.Find(&users)
}
func watchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error {
if !isWrite || !setting.Service.AutoWatchOnChanges {
return nil
}
watch, err := GetWatch(ctx, userID, repoID)
if err != nil {
return err
}
if watch.Mode != WatchModeNone {
return nil
}
return watchRepoMode(ctx, watch, WatchModeAuto)
}
// WatchIfAuto subscribes to repo if AutoWatchOnChanges is set
func WatchIfAuto(userID, repoID int64, isWrite bool) error {
return watchIfAuto(db.DefaultContext, userID, repoID, isWrite)
}

139
models/repo/watch_test.go Normal file
View File

@ -0,0 +1,139 @@
// Copyright 2017 The Gitea 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 repo
import (
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestIsWatching(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
assert.True(t, IsWatching(1, 1))
assert.True(t, IsWatching(4, 1))
assert.True(t, IsWatching(11, 1))
assert.False(t, IsWatching(1, 5))
assert.False(t, IsWatching(8, 1))
assert.False(t, IsWatching(unittest.NonexistentID, unittest.NonexistentID))
}
func TestGetWatchers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
watches, err := GetWatchers(db.DefaultContext, repo.ID)
assert.NoError(t, err)
// One watchers are inactive, thus minus 1
assert.Len(t, watches, repo.NumWatches-1)
for _, watch := range watches {
assert.EqualValues(t, repo.ID, watch.RepoID)
}
watches, err = GetWatchers(db.DefaultContext, unittest.NonexistentID)
assert.NoError(t, err)
assert.Len(t, watches, 0)
}
func TestRepository_GetWatchers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
watchers, err := GetRepoWatchers(repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, repo.NumWatches)
for _, watcher := range watchers {
unittest.AssertExistsAndLoadBean(t, &Watch{UserID: watcher.ID, RepoID: repo.ID})
}
repo = unittest.AssertExistsAndLoadBean(t, &Repository{ID: 9}).(*Repository)
watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, 0)
}
func TestWatchIfAuto(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
watchers, err := GetRepoWatchers(repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, repo.NumWatches)
setting.Service.AutoWatchOnChanges = false
prevCount := repo.NumWatches
// Must not add watch
assert.NoError(t, WatchIfAuto(8, 1, true))
watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
// Should not add watch
assert.NoError(t, WatchIfAuto(10, 1, true))
watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
setting.Service.AutoWatchOnChanges = true
// Must not add watch
assert.NoError(t, WatchIfAuto(8, 1, true))
watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
// Should not add watch
assert.NoError(t, WatchIfAuto(12, 1, false))
watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
// Should add watch
assert.NoError(t, WatchIfAuto(12, 1, true))
watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount+1)
// Should remove watch, inhibit from adding auto
assert.NoError(t, WatchRepo(12, 1, false))
watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
// Must not add watch
assert.NoError(t, WatchIfAuto(12, 1, true))
watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
}
func TestWatchRepoMode(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 0)
assert.NoError(t, WatchRepoMode(12, 1, WatchModeAuto))
unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 1)
unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1, Mode: WatchModeAuto}, 1)
assert.NoError(t, WatchRepoMode(12, 1, WatchModeNormal))
unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 1)
unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1, Mode: WatchModeNormal}, 1)
assert.NoError(t, WatchRepoMode(12, 1, WatchModeDont))
unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 1)
unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1, Mode: WatchModeDont}, 1)
assert.NoError(t, WatchRepoMode(12, 1, WatchModeNone))
unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 0)
}