From 26095115f4ae90e3fdc6ab695978efd16e317f75 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 6 Jun 2022 16:01:49 +0800 Subject: [PATCH] Move some repository related code into sub package (#19711) * Move some repository related code into sub package * Move more repository functions out of models * Fix lint * Some performance optimization for webhooks and others * some refactors * Fix lint * Fix * Update modules/repository/delete.go Co-authored-by: delvh * Fix test * Merge * Fix test * Fix test * Fix test * Fix test Co-authored-by: delvh --- cmd/admin.go | 5 +- models/action.go | 2 +- models/asymkey/ssh_key_deploy.go | 7 + models/db/engine.go | 5 - models/issue.go | 58 +- models/issue_list.go | 2 +- models/issue_user.go | 2 +- models/lfs.go | 13 +- models/notification.go | 10 +- models/org.go | 2 +- models/repo.go | 403 +--------- models/repo/attachment_test.go | 33 +- models/repo/collaboration_test.go | 13 +- models/repo/fork.go | 50 ++ models/repo/fork_test.go | 11 +- models/repo/main_test.go | 21 +- models/repo/mirror.go | 13 +- models/repo/pushmirror_test.go | 13 +- models/repo/redirect_test.go | 31 +- models/repo/repo_list.go | 665 ++++++++++++++++- models/{ => repo}/repo_list_test.go | 91 +-- models/repo/repo_test.go | 21 +- models/repo/star_test.go | 29 +- models/repo/topic_test.go | 41 +- models/repo/update.go | 8 + models/repo/user_repo.go | 122 +++ models/repo/user_repo_test.go | 74 ++ models/repo/watch_test.go | 87 +-- models/repo/wiki_test.go | 13 +- models/repo_generate.go | 118 --- models/repo_list.go | 704 ------------------ models/repo_test.go | 119 --- models/webhook/webhook.go | 8 + modules/context/repo.go | 5 +- modules/indexer/issues/indexer.go | 4 +- modules/repository/create.go | 115 ++- modules/repository/create_test.go | 21 + modules/repository/delete.go | 33 + modules/repository/fork.go | 31 + modules/repository/generate.go | 65 +- .../repository/generate_test.go | 2 +- modules/repository/init.go | 2 +- modules/repository/repo.go | 25 +- routers/api/v1/repo/collaborators.go | 4 +- routers/api/v1/repo/issue.go | 7 +- routers/api/v1/repo/repo.go | 10 +- routers/api/v1/user/repo.go | 8 +- routers/web/explore/code.go | 4 +- routers/web/explore/repo.go | 3 +- routers/web/org/home.go | 3 +- routers/web/org/setting.go | 5 +- routers/web/repo/attachment.go | 4 +- routers/web/repo/compare.go | 4 +- routers/web/repo/issue.go | 15 +- routers/web/repo/pull.go | 2 +- routers/web/repo/repo.go | 10 +- routers/web/repo/setting.go | 8 +- routers/web/repo/view.go | 3 +- routers/web/user/home.go | 10 +- routers/web/user/home_test.go | 4 +- routers/web/user/package.go | 3 +- routers/web/user/profile.go | 6 +- routers/web/user/setting/profile.go | 5 +- services/gitdiff/gitdiff.go | 2 +- services/mailer/mail_issue.go | 2 +- services/mirror/mirror_pull.go | 3 +- services/repository/adopt.go | 6 +- services/repository/check.go | 3 +- services/repository/fork.go | 10 +- services/repository/hooks.go | 27 + services/repository/push.go | 2 +- services/repository/repository.go | 40 + services/repository/repository_test.go | 43 ++ services/repository/review.go | 24 + services/repository/review_test.go | 28 + services/repository/template.go | 25 +- 76 files changed, 1756 insertions(+), 1674 deletions(-) rename models/{ => repo}/repo_list_test.go (57%) create mode 100644 models/repo/user_repo_test.go delete mode 100644 models/repo_generate.go delete mode 100644 models/repo_list.go create mode 100644 modules/repository/delete.go create mode 100644 modules/repository/fork.go rename models/repo_generate_test.go => modules/repository/generate_test.go (98%) create mode 100644 services/repository/repository_test.go create mode 100644 services/repository/review.go create mode 100644 services/repository/review_test.go diff --git a/cmd/admin.go b/cmd/admin.go index 0629dfc2d..32f9a95a6 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -17,6 +17,7 @@ import ( asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/graceful" @@ -722,9 +723,9 @@ func runRepoSyncReleases(_ *cli.Context) error { log.Trace("Synchronizing repository releases (this may take a while)") for page := 1; ; page++ { - repos, count, err := models.SearchRepositoryByName(&models.SearchRepoOptions{ + repos, count, err := repo_model.SearchRepositoryByName(&repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ - PageSize: models.RepositoryListDefaultPageSize, + PageSize: repo_model.RepositoryListDefaultPageSize, Page: page, }, Private: true, diff --git a/models/action.go b/models/action.go index 87bfcbbba..882bc59d8 100644 --- a/models/action.go +++ b/models/action.go @@ -393,7 +393,7 @@ func activityQueryCondition(opts GetFeedsOptions) (builder.Cond, error) { // check readable repositories by doer/actor if opts.Actor == nil || !opts.Actor.IsAdmin { - cond = cond.And(builder.In("repo_id", AccessibleRepoIDsQuery(opts.Actor))) + cond = cond.And(builder.In("repo_id", repo_model.AccessibleRepoIDsQuery(opts.Actor))) } if opts.RequestedRepo != nil { diff --git a/models/asymkey/ssh_key_deploy.go b/models/asymkey/ssh_key_deploy.go index 22fcefff6..fd8388e61 100644 --- a/models/asymkey/ssh_key_deploy.go +++ b/models/asymkey/ssh_key_deploy.go @@ -190,6 +190,13 @@ func GetDeployKeyByRepo(ctx context.Context, keyID, repoID int64) (*DeployKey, e return key, nil } +// IsDeployKeyExistByKeyID return true if there is at least one deploykey with the key id +func IsDeployKeyExistByKeyID(ctx context.Context, keyID int64) (bool, error) { + return db.GetEngine(ctx). + Where("key_id = ?", keyID). + Get(new(DeployKey)) +} + // UpdateDeployKeyCols updates deploy key information in the specified columns. func UpdateDeployKeyCols(key *DeployKey, cols ...string) error { _, err := db.GetEngine(db.DefaultContext).ID(key.ID).Cols(cols...).Update(key) diff --git a/models/db/engine.go b/models/db/engine.go index 23eb59dcf..8a3b4b206 100755 --- a/models/db/engine.go +++ b/models/db/engine.go @@ -271,11 +271,6 @@ func MaxBatchInsertSize(bean interface{}) int { return 999 / len(t.ColumnsSeq()) } -// Count returns records number according struct's fields as database query conditions -func Count(bean interface{}) (int64, error) { - return x.Count(bean) -} - // IsTableNotEmpty returns true if table has at least one record func IsTableNotEmpty(tableName string) (bool, error) { return x.Table(tableName).Exist() diff --git a/models/issue.go b/models/issue.go index 4150d66a6..a22c11552 100644 --- a/models/issue.go +++ b/models/issue.go @@ -1343,6 +1343,48 @@ func (opts *IssuesOptions) setupSessionNoLimit(sess *xorm.Session) { } } +// teamUnitsRepoCond returns query condition for those repo id in the special org team with special units access +func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Type) builder.Cond { + return builder.In(id, + builder.Select("repo_id").From("team_repo").Where( + builder.Eq{ + "team_id": teamID, + }.And( + builder.Or( + // Check if the user is member of the team. + builder.In( + "team_id", builder.Select("team_id").From("team_user").Where( + builder.Eq{ + "uid": userID, + }, + ), + ), + // Check if the user is in the owner team of the organisation. + builder.Exists(builder.Select("team_id").From("team_user"). + Where(builder.Eq{ + "org_id": orgID, + "team_id": builder.Select("id").From("team").Where( + builder.Eq{ + "org_id": orgID, + "lower_name": strings.ToLower(organization.OwnerTeamName), + }), + "uid": userID, + }), + ), + )).And( + builder.In( + "team_id", builder.Select("team_id").From("team_unit").Where( + builder.Eq{ + "`team_unit`.org_id": orgID, + }.And( + builder.In("`team_unit`.type", units), + ), + ), + ), + ), + )) +} + // issuePullAccessibleRepoCond userID must not be zero, this condition require join repository table func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organization.Organization, team *organization.Team, isPull bool) builder.Cond { cond := builder.NewCond() @@ -1356,19 +1398,19 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organizati } else { cond = cond.And( builder.Or( - userOrgUnitRepoCond(repoIDstr, userID, org.ID, unitType), // team member repos - userOrgPublicUnitRepoCond(userID, org.ID), // user org public non-member repos, TODO: check repo has issues + repo_model.UserOrgUnitRepoCond(repoIDstr, userID, org.ID, unitType), // team member repos + repo_model.UserOrgPublicUnitRepoCond(userID, org.ID), // user org public non-member repos, TODO: check repo has issues ), ) } } else { cond = cond.And( builder.Or( - userOwnedRepoCond(userID), // owned repos - userCollaborationRepoCond(repoIDstr, userID), // collaboration repos - userAssignedRepoCond(repoIDstr, userID), // user has been assigned accessible public repos - userMentionedRepoCond(repoIDstr, userID), // user has been mentioned accessible public repos - userCreateIssueRepoCond(repoIDstr, userID, isPull), // user has created issue/pr accessible public repos + repo_model.UserOwnedRepoCond(userID), // owned repos + repo_model.UserCollaborationRepoCond(repoIDstr, userID), // collaboration repos + repo_model.UserAssignedRepoCond(repoIDstr, userID), // user has been assigned accessible public repos + repo_model.UserMentionedRepoCond(repoIDstr, userID), // user has been mentioned accessible public repos + repo_model.UserCreateIssueRepoCond(repoIDstr, userID, isPull), // user has created issue/pr accessible public repos ), ) } @@ -1434,7 +1476,7 @@ func GetRepoIDsForIssuesOptions(opts *IssuesOptions, user *user_model.User) ([]i opts.setupSessionNoLimit(sess) - accessCond := accessibleRepositoryCondition(user) + accessCond := repo_model.AccessibleRepositoryCondition(user) if err := sess.Where(accessCond). Distinct("issue.repo_id"). Table("issue"). diff --git a/models/issue_list.go b/models/issue_list.go index 31588c02a..a5fc095e1 100644 --- a/models/issue_list.go +++ b/models/issue_list.go @@ -75,7 +75,7 @@ func (issues IssueList) loadRepositories(ctx context.Context) ([]*repo_model.Rep } } } - return valuesRepository(repoMaps), nil + return repo_model.ValuesRepository(repoMaps), nil } // LoadRepositories loads issues' all repositories diff --git a/models/issue_user.go b/models/issue_user.go index 0b1f8204b..19c64094a 100644 --- a/models/issue_user.go +++ b/models/issue_user.go @@ -26,7 +26,7 @@ func init() { } func newIssueUsers(ctx context.Context, repo *repo_model.Repository, issue *Issue) error { - assignees, err := getRepoAssignees(ctx, repo) + assignees, err := repo_model.GetRepoAssignees(ctx, repo) if err != nil { return fmt.Errorf("getAssignees: %v", err) } diff --git a/models/lfs.go b/models/lfs.go index 037ed8556..d9eea6bb8 100644 --- a/models/lfs.go +++ b/models/lfs.go @@ -142,7 +142,7 @@ func LFSObjectAccessible(user *user_model.User, oid string) (bool, error) { count, err := db.GetEngine(db.DefaultContext).Count(&LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}}) return count > 0, err } - cond := accessibleRepositoryCondition(user) + cond := repo_model.AccessibleRepositoryCondition(user) count, err := db.GetEngine(db.DefaultContext).Where(cond).Join("INNER", "repository", "`lfs_meta_object`.repository_id = `repository`.id").Count(&LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}}) return count > 0, err } @@ -173,7 +173,7 @@ func LFSAutoAssociate(metas []*LFSMetaObject, user *user_model.User, repoID int6 newMetas := make([]*LFSMetaObject, 0, len(metas)) cond := builder.In( "`lfs_meta_object`.repository_id", - builder.Select("`repository`.id").From("repository").Where(accessibleRepositoryCondition(user)), + builder.Select("`repository`.id").From("repository").Where(repo_model.AccessibleRepositoryCondition(user)), ) err = sess.Cols("oid").Where(cond).In("oid", oids...).GroupBy("oid").Find(&newMetas) if err != nil { @@ -246,3 +246,12 @@ func CopyLFS(ctx context.Context, newRepo, oldRepo *repo_model.Repository) error return nil } + +// GetRepoLFSSize return a repository's lfs files size +func GetRepoLFSSize(ctx context.Context, repoID int64) (int64, error) { + lfsSize, err := db.GetEngine(ctx).Where("repository_id = ?", repoID).SumInt(new(LFSMetaObject), "size") + if err != nil { + return 0, fmt.Errorf("updateSize: GetLFSMetaObjects: %v", err) + } + return lfsSize, nil +} diff --git a/models/notification.go b/models/notification.go index 548362d19..ac5abc6f9 100644 --- a/models/notification.go +++ b/models/notification.go @@ -266,10 +266,10 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n return err } - if issue.IsPull && !checkRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) { + if issue.IsPull && !CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) { continue } - if !issue.IsPull && !checkRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) { + if !issue.IsPull && !CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) { continue } @@ -510,9 +510,9 @@ func (nl NotificationList) getPendingRepoIDs() []int64 { } // LoadRepos loads repositories from database -func (nl NotificationList) LoadRepos() (RepositoryList, []int, error) { +func (nl NotificationList) LoadRepos() (repo_model.RepositoryList, []int, error) { if len(nl) == 0 { - return RepositoryList{}, []int{}, nil + return repo_model.RepositoryList{}, []int{}, nil } repoIDs := nl.getPendingRepoIDs() @@ -548,7 +548,7 @@ func (nl NotificationList) LoadRepos() (RepositoryList, []int, error) { failed := []int{} - reposList := make(RepositoryList, 0, len(repoIDs)) + reposList := make(repo_model.RepositoryList, 0, len(repoIDs)) for i, notification := range nl { if notification.Repository == nil { notification.Repository = repos[notification.RepoID] diff --git a/models/org.go b/models/org.go index 681b367f4..009fe758b 100644 --- a/models/org.go +++ b/models/org.go @@ -54,7 +54,7 @@ func GetUserOrgsList(user *user_model.User) ([]*MinimalOrg, error) { Join("LEFT", builder. Select("id as repo_id, owner_id as repo_owner_id"). From("repository"). - Where(accessibleRepositoryCondition(user)), "`repository`.repo_owner_id = `team`.org_id"). + Where(repo_model.AccessibleRepositoryCondition(user)), "`repository`.repo_owner_id = `team`.org_id"). Where("`team_user`.uid = ?", user.ID). GroupBy(groupByStr) diff --git a/models/repo.go b/models/repo.go index 6c3dca41b..fff9cc527 100644 --- a/models/repo.go +++ b/models/repo.go @@ -8,11 +8,7 @@ package models import ( "context" "fmt" - "os" - "path" "strconv" - "strings" - "unicode/utf8" _ "image/jpeg" // Needed for jpeg support @@ -47,11 +43,7 @@ func NewRepoContext() { } // CheckRepoUnitUser check whether user could visit the unit of this repository -func CheckRepoUnitUser(repo *repo_model.Repository, user *user_model.User, unitType unit.Type) bool { - return checkRepoUnitUser(db.DefaultContext, repo, user, unitType) -} - -func checkRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *user_model.User, unitType unit.Type) bool { +func CheckRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *user_model.User, unitType unit.Type) bool { if user != nil && user.IsAdmin { return true } @@ -64,241 +56,6 @@ func checkRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *u return perm.CanRead(unitType) } -func getRepoAssignees(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) { - if err = repo.GetOwner(ctx); err != nil { - return nil, err - } - - e := db.GetEngine(ctx) - userIDs := make([]int64, 0, 10) - if err = e.Table("access"). - Where("repo_id = ? AND mode >= ?", repo.ID, perm.AccessModeWrite). - Select("user_id"). - Find(&userIDs); err != nil { - return nil, err - } - - additionalUserIDs := make([]int64, 0, 10) - if err = e.Table("team_user"). - Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). - Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id"). - Where("`team_repo`.repo_id = ? AND `team_unit`.access_mode >= ?", repo.ID, perm.AccessModeWrite). - Distinct("`team_user`.uid"). - Select("`team_user`.uid"). - Find(&additionalUserIDs); err != nil { - return nil, err - } - - uidMap := map[int64]bool{} - i := 0 - for _, uid := range userIDs { - if uidMap[uid] { - continue - } - uidMap[uid] = true - userIDs[i] = uid - i++ - } - userIDs = userIDs[:i] - userIDs = append(userIDs, additionalUserIDs...) - - for _, uid := range additionalUserIDs { - if uidMap[uid] { - continue - } - userIDs[i] = uid - i++ - } - userIDs = userIDs[:i] - - // Leave a seat for owner itself to append later, but if owner is an organization - // and just waste 1 unit is cheaper than re-allocate memory once. - users := make([]*user_model.User, 0, len(userIDs)+1) - if len(userIDs) > 0 { - if err = e.In("id", userIDs).Find(&users); err != nil { - return nil, err - } - } - if !repo.Owner.IsOrganization() && !uidMap[repo.OwnerID] { - users = append(users, repo.Owner) - } - - return users, nil -} - -// GetRepoAssignees returns all users that have write access and can be assigned to issues -// of the repository, -func GetRepoAssignees(repo *repo_model.Repository) (_ []*user_model.User, err error) { - return getRepoAssignees(db.DefaultContext, repo) -} - -func getReviewers(ctx context.Context, repo *repo_model.Repository, doerID, posterID int64) ([]*user_model.User, error) { - // Get the owner of the repository - this often already pre-cached and if so saves complexity for the following queries - if err := repo.GetOwner(ctx); err != nil { - return nil, err - } - - cond := builder.And(builder.Neq{"`user`.id": posterID}) - - if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate { - // This a private repository: - // Anyone who can read the repository is a requestable reviewer - - cond = cond.And(builder.In("`user`.id", - builder.Select("user_id").From("access").Where( - builder.Eq{"repo_id": repo.ID}. - And(builder.Gte{"mode": perm.AccessModeRead}), - ), - )) - - if repo.Owner.Type == user_model.UserTypeIndividual && repo.Owner.ID != posterID { - // as private *user* repos don't generate an entry in the `access` table, - // the owner of a private repo needs to be explicitly added. - cond = cond.Or(builder.Eq{"`user`.id": repo.Owner.ID}) - } - - } else { - // This is a "public" repository: - // Any user that has read access, is a watcher or organization member can be requested to review - cond = cond.And(builder.And(builder.In("`user`.id", - builder.Select("user_id").From("access"). - Where(builder.Eq{"repo_id": repo.ID}. - And(builder.Gte{"mode": perm.AccessModeRead})), - ).Or(builder.In("`user`.id", - builder.Select("user_id").From("watch"). - Where(builder.Eq{"repo_id": repo.ID}. - And(builder.In("mode", repo_model.WatchModeNormal, repo_model.WatchModeAuto))), - ).Or(builder.In("`user`.id", - builder.Select("uid").From("org_user"). - Where(builder.Eq{"org_id": repo.OwnerID}), - ))))) - } - - users := make([]*user_model.User, 0, 8) - return users, db.GetEngine(ctx).Where(cond).OrderBy("name").Find(&users) -} - -// GetReviewers get all users can be requested to review: -// * for private repositories this returns all users that have read access or higher to the repository. -// * for public repositories this returns all users that have read access or higher to the repository, -// all repo watchers and all organization members. -// TODO: may be we should have a busy choice for users to block review request to them. -func GetReviewers(repo *repo_model.Repository, doerID, posterID int64) ([]*user_model.User, error) { - return getReviewers(db.DefaultContext, repo, doerID, posterID) -} - -// GetReviewerTeams get all teams can be requested to review -func GetReviewerTeams(repo *repo_model.Repository) ([]*organization.Team, error) { - if err := repo.GetOwner(db.DefaultContext); err != nil { - return nil, err - } - if !repo.Owner.IsOrganization() { - return nil, nil - } - - teams, err := organization.GetTeamsWithAccessToRepo(db.DefaultContext, repo.OwnerID, repo.ID, perm.AccessModeRead) - if err != nil { - return nil, err - } - - return teams, err -} - -// UpdateRepoSize updates the repository size, calculating it using util.GetDirectorySize -func UpdateRepoSize(ctx context.Context, repo *repo_model.Repository) error { - size, err := util.GetDirectorySize(repo.RepoPath()) - if err != nil { - return fmt.Errorf("updateSize: %v", err) - } - - lfsSize, err := db.GetEngine(ctx).Where("repository_id = ?", repo.ID).SumInt(new(LFSMetaObject), "size") - if err != nil { - return fmt.Errorf("updateSize: GetLFSMetaObjects: %v", err) - } - - repo.Size = size + lfsSize - _, err = db.GetEngine(ctx).ID(repo.ID).Cols("size").NoAutoTime().Update(repo) - return err -} - -// CanUserForkRepo returns true if specified user can fork repository. -func CanUserForkRepo(user *user_model.User, repo *repo_model.Repository) (bool, error) { - if user == nil { - return false, nil - } - if repo.OwnerID != user.ID && !repo_model.HasForkedRepo(user.ID, repo.ID) { - return true, nil - } - ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(user.ID) - if err != nil { - return false, err - } - for _, org := range ownedOrgs { - if repo.OwnerID != org.ID && !repo_model.HasForkedRepo(org.ID, repo.ID) { - return true, nil - } - } - return false, nil -} - -// FindUserOrgForks returns the forked repositories for one user from a repository -func FindUserOrgForks(ctx context.Context, repoID, userID int64) ([]*repo_model.Repository, error) { - cond := builder.And( - builder.Eq{"fork_id": repoID}, - builder.In("owner_id", - builder.Select("org_id"). - From("org_user"). - Where(builder.Eq{"uid": userID}), - ), - ) - - var repos []*repo_model.Repository - return repos, db.GetEngine(ctx).Table("repository").Where(cond).Find(&repos) -} - -// GetForksByUserAndOrgs return forked repos of the user and owned orgs -func GetForksByUserAndOrgs(ctx context.Context, user *user_model.User, repo *repo_model.Repository) ([]*repo_model.Repository, error) { - var repoList []*repo_model.Repository - if user == nil { - return repoList, nil - } - forkedRepo, err := repo_model.GetUserFork(ctx, repo.ID, user.ID) - if err != nil { - return repoList, err - } - if forkedRepo != nil { - repoList = append(repoList, forkedRepo) - } - orgForks, err := FindUserOrgForks(ctx, repo.ID, user.ID) - if err != nil { - return nil, err - } - repoList = append(repoList, orgForks...) - return repoList, nil -} - -// CanUserDelete returns true if user could delete the repository -func CanUserDelete(repo *repo_model.Repository, user *user_model.User) (bool, error) { - if user.IsAdmin || user.ID == repo.OwnerID { - return true, nil - } - - if err := repo.GetOwner(db.DefaultContext); err != nil { - return false, err - } - - if repo.Owner.IsOrganization() { - isOwner, err := organization.OrgFromUser(repo.Owner).IsOwnedBy(user.ID) - if err != nil { - return false, err - } else if isOwner { - return true, nil - } - } - - return false, nil -} - // CreateRepoOptions contains the create repository options type CreateRepoOptions struct { Name string @@ -441,126 +198,6 @@ func CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_ return nil } -// CheckDaemonExportOK creates/removes git-daemon-export-ok for git-daemon... -func CheckDaemonExportOK(ctx context.Context, repo *repo_model.Repository) error { - if err := repo.GetOwner(ctx); err != nil { - return err - } - - // Create/Remove git-daemon-export-ok for git-daemon... - daemonExportFile := path.Join(repo.RepoPath(), `git-daemon-export-ok`) - - isExist, err := util.IsExist(daemonExportFile) - if err != nil { - log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err) - return err - } - - isPublic := !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePublic - if !isPublic && isExist { - if err = util.Remove(daemonExportFile); err != nil { - log.Error("Failed to remove %s: %v", daemonExportFile, err) - } - } else if isPublic && !isExist { - if f, err := os.Create(daemonExportFile); err != nil { - log.Error("Failed to create %s: %v", daemonExportFile, err) - } else { - f.Close() - } - } - - return nil -} - -// IncrementRepoForkNum increment repository fork number -func IncrementRepoForkNum(ctx context.Context, repoID int64) error { - _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", repoID) - return err -} - -// DecrementRepoForkNum decrement repository fork number -func DecrementRepoForkNum(ctx context.Context, repoID int64) error { - _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repoID) - return err -} - -// UpdateRepositoryCtx updates a repository with db context -func UpdateRepositoryCtx(ctx context.Context, repo *repo_model.Repository, visibilityChanged bool) (err error) { - repo.LowerName = strings.ToLower(repo.Name) - - if utf8.RuneCountInString(repo.Description) > 255 { - repo.Description = string([]rune(repo.Description)[:255]) - } - if utf8.RuneCountInString(repo.Website) > 255 { - repo.Website = string([]rune(repo.Website)[:255]) - } - - e := db.GetEngine(ctx) - - if _, err = e.ID(repo.ID).AllCols().Update(repo); err != nil { - return fmt.Errorf("update: %v", err) - } - - if err = UpdateRepoSize(ctx, repo); err != nil { - log.Error("Failed to update size for repository: %v", err) - } - - if visibilityChanged { - if err = repo.GetOwner(ctx); err != nil { - return fmt.Errorf("getOwner: %v", err) - } - if repo.Owner.IsOrganization() { - // Organization repository need to recalculate access table when visibility is changed. - if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil { - return fmt.Errorf("recalculateTeamAccesses: %v", err) - } - } - - // If repo has become private, we need to set its actions to private. - if repo.IsPrivate { - _, err = e.Where("repo_id = ?", repo.ID).Cols("is_private").Update(&Action{ - IsPrivate: true, - }) - if err != nil { - return err - } - } - - // Create/Remove git-daemon-export-ok for git-daemon... - if err := CheckDaemonExportOK(ctx, repo); err != nil { - return err - } - - forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID) - if err != nil { - return fmt.Errorf("GetRepositoriesByForkID: %v", err) - } - for i := range forkRepos { - forkRepos[i].IsPrivate = repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate - if err = UpdateRepositoryCtx(ctx, forkRepos[i], true); err != nil { - return fmt.Errorf("updateRepository[%d]: %v", forkRepos[i].ID, err) - } - } - } - - return nil -} - -// UpdateRepository updates a repository -func UpdateRepository(repo *repo_model.Repository, visibilityChanged bool) (err error) { - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - if err = UpdateRepositoryCtx(ctx, repo, visibilityChanged); err != nil { - return fmt.Errorf("updateRepository: %v", err) - } - - return committer.Commit() -} - // DeleteRepository deletes a repository for a user or organization. // make sure if you call this func to close open sessions (sqlite will otherwise get a deadlock) func DeleteRepository(doer *user_model.User, uid, repoID int64) error { @@ -1052,14 +689,14 @@ func CheckRepoStats(ctx context.Context) error { continue } - rawResult, err := db.GetEngine(db.DefaultContext).Query("SELECT COUNT(*) FROM `repository` WHERE fork_id=?", repo.ID) + rawResult, err := e.Query("SELECT COUNT(*) FROM `repository` WHERE fork_id=?", repo.ID) if err != nil { log.Error("Select count of forks[%d]: %v", repo.ID, err) continue } repo.NumForks = int(parseCountResult(rawResult)) - if err = UpdateRepository(repo, false); err != nil { + if _, err = e.ID(repo.ID).Cols("num_forks").Update(repo); err != nil { log.Error("UpdateRepository[%d]: %v", id, err) continue } @@ -1130,30 +767,6 @@ func DoctorUserStarNum() (err error) { return } -// LinkedRepository returns the linked repo if any -func LinkedRepository(a *repo_model.Attachment) (*repo_model.Repository, unit.Type, error) { - if a.IssueID != 0 { - iss, err := GetIssueByID(a.IssueID) - if err != nil { - return nil, unit.TypeIssues, err - } - repo, err := repo_model.GetRepositoryByID(iss.RepoID) - unitType := unit.TypeIssues - if iss.IsPull { - unitType = unit.TypePullRequests - } - return repo, unitType, err - } else if a.ReleaseID != 0 { - rel, err := GetReleaseByID(db.DefaultContext, a.ReleaseID) - if err != nil { - return nil, unit.TypeReleases, err - } - repo, err := repo_model.GetRepositoryByID(rel.RepoID) - return repo, unit.TypeReleases, err - } - return nil, -1, nil -} - // DeleteDeployKey delete deploy keys func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error { key, err := asymkey_model.GetDeployKeyByID(ctx, id) @@ -1164,8 +777,6 @@ func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error return fmt.Errorf("GetDeployKeyByID: %v", err) } - sess := db.GetEngine(ctx) - // Check if user has access to delete this key. if !doer.IsAdmin { repo, err := repo_model.GetRepositoryByIDCtx(ctx, key.RepoID) @@ -1184,14 +795,14 @@ func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error } } - if _, err = sess.ID(key.ID).Delete(new(asymkey_model.DeployKey)); err != nil { + if _, err := db.DeleteByBean(ctx, &asymkey_model.DeployKey{ + ID: key.ID, + }); err != nil { return fmt.Errorf("delete deploy key [%d]: %v", key.ID, err) } // Check if this is the last reference to same key content. - has, err := sess. - Where("key_id = ?", key.KeyID). - Get(new(asymkey_model.DeployKey)) + has, err := asymkey_model.IsDeployKeyExistByKeyID(ctx, key.KeyID) if err != nil { return err } else if !has { diff --git a/models/repo/attachment_test.go b/models/repo/attachment_test.go index da486fdb2..d7c2f529d 100644 --- a/models/repo/attachment_test.go +++ b/models/repo/attachment_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -16,7 +17,7 @@ import ( func TestIncreaseDownloadCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - attachment, err := GetAttachmentByUUID(db.DefaultContext, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11") + attachment, err := repo_model.GetAttachmentByUUID(db.DefaultContext, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11") assert.NoError(t, err) assert.Equal(t, int64(0), attachment.DownloadCount) @@ -24,7 +25,7 @@ func TestIncreaseDownloadCount(t *testing.T) { err = attachment.IncreaseDownloadCount() assert.NoError(t, err) - attachment, err = GetAttachmentByUUID(db.DefaultContext, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11") + attachment, err = repo_model.GetAttachmentByUUID(db.DefaultContext, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11") assert.NoError(t, err) assert.Equal(t, int64(1), attachment.DownloadCount) } @@ -33,11 +34,11 @@ func TestGetByCommentOrIssueID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) // count of attachments from issue ID - attachments, err := GetAttachmentsByIssueID(db.DefaultContext, 1) + attachments, err := repo_model.GetAttachmentsByIssueID(db.DefaultContext, 1) assert.NoError(t, err) assert.Len(t, attachments, 1) - attachments, err = GetAttachmentsByCommentID(db.DefaultContext, 1) + attachments, err = repo_model.GetAttachmentsByCommentID(db.DefaultContext, 1) assert.NoError(t, err) assert.Len(t, attachments, 2) } @@ -45,33 +46,33 @@ func TestGetByCommentOrIssueID(t *testing.T) { func TestDeleteAttachments(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - count, err := DeleteAttachmentsByIssue(4, false) + count, err := repo_model.DeleteAttachmentsByIssue(4, false) assert.NoError(t, err) assert.Equal(t, 2, count) - count, err = DeleteAttachmentsByComment(2, false) + count, err = repo_model.DeleteAttachmentsByComment(2, false) assert.NoError(t, err) assert.Equal(t, 2, count) - err = DeleteAttachment(&Attachment{ID: 8}, false) + err = repo_model.DeleteAttachment(&repo_model.Attachment{ID: 8}, false) assert.NoError(t, err) - attachment, err := GetAttachmentByUUID(db.DefaultContext, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a18") + attachment, err := repo_model.GetAttachmentByUUID(db.DefaultContext, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a18") assert.Error(t, err) - assert.True(t, IsErrAttachmentNotExist(err)) + assert.True(t, repo_model.IsErrAttachmentNotExist(err)) assert.Nil(t, attachment) } func TestGetAttachmentByID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - attach, err := GetAttachmentByID(db.DefaultContext, 1) + attach, err := repo_model.GetAttachmentByID(db.DefaultContext, 1) assert.NoError(t, err) assert.Equal(t, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", attach.UUID) } func TestAttachment_DownloadURL(t *testing.T) { - attach := &Attachment{ + attach := &repo_model.Attachment{ UUID: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", ID: 1, } @@ -81,20 +82,20 @@ func TestAttachment_DownloadURL(t *testing.T) { func TestUpdateAttachment(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - attach, err := GetAttachmentByID(db.DefaultContext, 1) + attach, err := repo_model.GetAttachmentByID(db.DefaultContext, 1) assert.NoError(t, err) assert.Equal(t, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", attach.UUID) attach.Name = "new_name" - assert.NoError(t, UpdateAttachment(db.DefaultContext, attach)) + assert.NoError(t, repo_model.UpdateAttachment(db.DefaultContext, attach)) - unittest.AssertExistsAndLoadBean(t, &Attachment{Name: "new_name"}) + unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{Name: "new_name"}) } func TestGetAttachmentsByUUIDs(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - attachList, err := GetAttachmentsByUUIDs(db.DefaultContext, []string{"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a17", "not-existing-uuid"}) + attachList, err := repo_model.GetAttachmentsByUUIDs(db.DefaultContext, []string{"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a17", "not-existing-uuid"}) assert.NoError(t, err) assert.Len(t, attachList, 2) assert.Equal(t, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", attachList[0].UUID) diff --git a/models/repo/collaboration_test.go b/models/repo/collaboration_test.go index a7d04498e..8cb7980a7 100644 --- a/models/repo/collaboration_test.go +++ b/models/repo/collaboration_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -16,10 +17,10 @@ import ( func TestRepository_GetCollaborators(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(repoID int64) { - repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository) - collaborators, err := GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}).(*repo_model.Repository) + collaborators, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{}) assert.NoError(t, err) - expectedLen, err := db.GetEngine(db.DefaultContext).Count(&Collaboration{RepoID: repoID}) + expectedLen, err := db.GetEngine(db.DefaultContext).Count(&repo_model.Collaboration{RepoID: repoID}) assert.NoError(t, err) assert.Len(t, collaborators, int(expectedLen)) for _, collaborator := range collaborators { @@ -37,8 +38,8 @@ func TestRepository_IsCollaborator(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(repoID, userID int64, expected bool) { - repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository) - actual, err := IsCollaborator(db.DefaultContext, repo.ID, userID) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}).(*repo_model.Repository) + actual, err := repo_model.IsCollaborator(db.DefaultContext, repo.ID, userID) assert.NoError(t, err) assert.Equal(t, expected, actual) } diff --git a/models/repo/fork.go b/models/repo/fork.go index b48126253..938bbae17 100644 --- a/models/repo/fork.go +++ b/models/repo/fork.go @@ -8,6 +8,8 @@ import ( "context" "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "xorm.io/builder" ) // GetRepositoriesByForkID returns all repositories with given fork ID. @@ -63,3 +65,51 @@ func GetForks(repo *Repository, listOptions db.ListOptions) ([]*Repository, erro forks := make([]*Repository, 0, listOptions.PageSize) return forks, sess.Find(&forks, &Repository{ForkID: repo.ID}) } + +// IncrementRepoForkNum increment repository fork number +func IncrementRepoForkNum(ctx context.Context, repoID int64) error { + _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", repoID) + return err +} + +// DecrementRepoForkNum decrement repository fork number +func DecrementRepoForkNum(ctx context.Context, repoID int64) error { + _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repoID) + return err +} + +// FindUserOrgForks returns the forked repositories for one user from a repository +func FindUserOrgForks(ctx context.Context, repoID, userID int64) ([]*Repository, error) { + cond := builder.And( + builder.Eq{"fork_id": repoID}, + builder.In("owner_id", + builder.Select("org_id"). + From("org_user"). + Where(builder.Eq{"uid": userID}), + ), + ) + + var repos []*Repository + return repos, db.GetEngine(ctx).Table("repository").Where(cond).Find(&repos) +} + +// GetForksByUserAndOrgs return forked repos of the user and owned orgs +func GetForksByUserAndOrgs(ctx context.Context, user *user_model.User, repo *Repository) ([]*Repository, error) { + var repoList []*Repository + if user == nil { + return repoList, nil + } + forkedRepo, err := GetUserFork(ctx, repo.ID, user.ID) + if err != nil { + return repoList, err + } + if forkedRepo != nil { + repoList = append(repoList, forkedRepo) + } + orgForks, err := FindUserOrgForks(ctx, repo.ID, user.ID) + if err != nil { + return nil, err + } + repoList = append(repoList, orgForks...) + return repoList, nil +} diff --git a/models/repo/fork_test.go b/models/repo/fork_test.go index 263aec4e3..9e08d8136 100644 --- a/models/repo/fork_test.go +++ b/models/repo/fork_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -17,17 +18,17 @@ func TestGetUserFork(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) // User13 has repo 11 forked from repo10 - repo, err := GetRepositoryByID(10) + repo, err := repo_model.GetRepositoryByID(10) assert.NoError(t, err) assert.NotNil(t, repo) - repo, err = GetUserFork(db.DefaultContext, repo.ID, 13) + repo, err = repo_model.GetUserFork(db.DefaultContext, repo.ID, 13) assert.NoError(t, err) assert.NotNil(t, repo) - repo, err = GetRepositoryByID(9) + repo, err = repo_model.GetRepositoryByID(9) assert.NoError(t, err) assert.NotNil(t, repo) - repo, err = GetUserFork(db.DefaultContext, repo.ID, 13) + repo, err = repo_model.GetUserFork(db.DefaultContext, repo.ID, 13) assert.NoError(t, err) assert.Nil(t, repo) } diff --git a/models/repo/main_test.go b/models/repo/main_test.go index 375d0e0df..eb04aa822 100644 --- a/models/repo/main_test.go +++ b/models/repo/main_test.go @@ -2,31 +2,22 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "path/filepath" "testing" + _ "code.gitea.io/gitea/models" // register table model + _ "code.gitea.io/gitea/models/perm/access" // register table model + _ "code.gitea.io/gitea/models/repo" // register table model + _ "code.gitea.io/gitea/models/user" // register table model + "code.gitea.io/gitea/models/unittest" ) func TestMain(m *testing.M) { unittest.MainTest(m, &unittest.TestOptions{ GiteaRootPath: filepath.Join("..", ".."), - FixtureFiles: []string{ - "attachment.yml", - "repo_archiver.yml", - "repository.yml", - "repo_unit.yml", - "repo_indexer_status.yml", - "repo_redirect.yml", - "watch.yml", - "star.yml", - "topic.yml", - "repo_topic.yml", - "user.yml", - "collaboration.yml", - }, }) } diff --git a/models/repo/mirror.go b/models/repo/mirror.go index 5d20b7f83..bd83d2442 100644 --- a/models/repo/mirror.go +++ b/models/repo/mirror.go @@ -123,8 +123,8 @@ func MirrorsIterate(limit int, f func(idx int, bean interface{}) error) error { } // InsertMirror inserts a mirror to database -func InsertMirror(mirror *Mirror) error { - _, err := db.GetEngine(db.DefaultContext).Insert(mirror) +func InsertMirror(ctx context.Context, mirror *Mirror) error { + _, err := db.GetEngine(ctx).Insert(mirror) return err } @@ -168,3 +168,12 @@ func (repos MirrorRepositoryList) loadAttributes(ctx context.Context) error { func (repos MirrorRepositoryList) LoadAttributes() error { return repos.loadAttributes(db.DefaultContext) } + +// GetUserMirrorRepositories returns a list of mirror repositories of given user. +func GetUserMirrorRepositories(userID int64) ([]*Repository, error) { + repos := make([]*Repository, 0, 10) + return repos, db.GetEngine(db.DefaultContext). + Where("owner_id = ?", userID). + And("is_mirror = ?", true). + Find(&repos) +} diff --git a/models/repo/pushmirror_test.go b/models/repo/pushmirror_test.go index 83cf86131..d36a48547 100644 --- a/models/repo/pushmirror_test.go +++ b/models/repo/pushmirror_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "time" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/timeutil" @@ -19,20 +20,20 @@ func TestPushMirrorsIterate(t *testing.T) { now := timeutil.TimeStampNow() - InsertPushMirror(&PushMirror{ + repo_model.InsertPushMirror(&repo_model.PushMirror{ RemoteName: "test-1", LastUpdateUnix: now, Interval: 1, }) long, _ := time.ParseDuration("24h") - InsertPushMirror(&PushMirror{ + repo_model.InsertPushMirror(&repo_model.PushMirror{ RemoteName: "test-2", LastUpdateUnix: now, Interval: long, }) - InsertPushMirror(&PushMirror{ + repo_model.InsertPushMirror(&repo_model.PushMirror{ RemoteName: "test-3", LastUpdateUnix: now, Interval: 0, @@ -40,8 +41,8 @@ func TestPushMirrorsIterate(t *testing.T) { time.Sleep(1 * time.Millisecond) - PushMirrorsIterate(1, func(idx int, bean interface{}) error { - m, ok := bean.(*PushMirror) + repo_model.PushMirrorsIterate(1, func(idx int, bean interface{}) error { + m, ok := bean.(*repo_model.PushMirror) assert.True(t, ok) assert.Equal(t, "test-1", m.RemoteName) assert.Equal(t, m.RemoteName, m.GetRemoteName()) diff --git a/models/repo/redirect_test.go b/models/repo/redirect_test.go index 2dca2cbbf..05b105cf6 100644 --- a/models/repo/redirect_test.go +++ b/models/repo/redirect_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -16,27 +17,27 @@ import ( func TestLookupRedirect(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - repoID, err := LookupRedirect(2, "oldrepo1") + repoID, err := repo_model.LookupRedirect(2, "oldrepo1") assert.NoError(t, err) assert.EqualValues(t, 1, repoID) - _, err = LookupRedirect(unittest.NonexistentID, "doesnotexist") - assert.True(t, IsErrRedirectNotExist(err)) + _, err = repo_model.LookupRedirect(unittest.NonexistentID, "doesnotexist") + assert.True(t, repo_model.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")) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + assert.NoError(t, repo_model.NewRedirect(db.DefaultContext, repo.OwnerID, repo.ID, repo.Name, "newreponame")) - unittest.AssertExistsAndLoadBean(t, &Redirect{ + unittest.AssertExistsAndLoadBean(t, &repo_model.Redirect{ OwnerID: repo.OwnerID, LowerName: repo.LowerName, RedirectRepoID: repo.ID, }) - unittest.AssertExistsAndLoadBean(t, &Redirect{ + unittest.AssertExistsAndLoadBean(t, &repo_model.Redirect{ OwnerID: repo.OwnerID, LowerName: "oldrepo1", RedirectRepoID: repo.ID, @@ -47,15 +48,15 @@ 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")) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + assert.NoError(t, repo_model.NewRedirect(db.DefaultContext, repo.OwnerID, repo.ID, repo.Name, "oldrepo1")) - unittest.AssertExistsAndLoadBean(t, &Redirect{ + unittest.AssertExistsAndLoadBean(t, &repo_model.Redirect{ OwnerID: repo.OwnerID, LowerName: repo.LowerName, RedirectRepoID: repo.ID, }) - unittest.AssertNotExistsBean(t, &Redirect{ + unittest.AssertNotExistsBean(t, &repo_model.Redirect{ OwnerID: repo.OwnerID, LowerName: "oldrepo1", RedirectRepoID: repo.ID, @@ -66,10 +67,10 @@ 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")) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) + assert.NoError(t, repo_model.NewRedirect(db.DefaultContext, repo.OwnerID, repo.ID, repo.Name, "newreponame")) - unittest.AssertExistsAndLoadBean(t, &Redirect{ + unittest.AssertExistsAndLoadBean(t, &repo_model.Redirect{ OwnerID: repo.OwnerID, LowerName: repo.LowerName, RedirectRepoID: repo.ID, diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 23cdd6cad..1bec35d57 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -5,18 +5,21 @@ package repo import ( - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/setting" -) + "context" + "fmt" + "strings" -// GetUserMirrorRepositories returns a list of mirror repositories of given user. -func GetUserMirrorRepositories(userID int64) ([]*Repository, error) { - repos := make([]*Repository, 0, 10) - return repos, db.GetEngine(db.DefaultContext). - Where("owner_id = ?", userID). - And("is_mirror = ?", true). - Find(&repos) -} + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) // IterateRepository iterate repositories func IterateRepository(f func(repo *Repository) error) error { @@ -45,3 +48,643 @@ func IterateRepository(f func(repo *Repository) error) error { func FindReposMapByIDs(repoIDs []int64, res map[int64]*Repository) error { return db.GetEngine(db.DefaultContext).In("id", repoIDs).Find(&res) } + +// RepositoryListDefaultPageSize is the default number of repositories +// to load in memory when running administrative tasks on all (or almost +// all) of them. +// The number should be low enough to avoid filling up all RAM with +// repository data... +const RepositoryListDefaultPageSize = 64 + +// RepositoryList contains a list of repositories +type RepositoryList []*Repository + +func (repos RepositoryList) Len() int { + return len(repos) +} + +func (repos RepositoryList) Less(i, j int) bool { + return repos[i].FullName() < repos[j].FullName() +} + +func (repos RepositoryList) Swap(i, j int) { + repos[i], repos[j] = repos[j], repos[i] +} + +// ValuesRepository converts a repository map to a list +// FIXME: Remove in favor of maps.values when MIN_GO_VERSION >= 1.18 +func ValuesRepository(m map[int64]*Repository) []*Repository { + values := make([]*Repository, 0, len(m)) + for _, v := range m { + values = append(values, v) + } + return values +} + +// RepositoryListOfMap make list from values of map +func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList { + return RepositoryList(ValuesRepository(repoMap)) +} + +func (repos RepositoryList) loadAttributes(ctx context.Context) error { + if len(repos) == 0 { + return nil + } + + set := make(map[int64]struct{}) + repoIDs := make([]int64, len(repos)) + for i := range repos { + set[repos[i].OwnerID] = struct{}{} + repoIDs[i] = repos[i].ID + } + + // Load owners. + users := make(map[int64]*user_model.User, len(set)) + if err := db.GetEngine(ctx). + Where("id > 0"). + In("id", container.KeysInt64(set)). + Find(&users); err != nil { + return fmt.Errorf("find users: %v", err) + } + for i := range repos { + repos[i].Owner = users[repos[i].OwnerID] + } + + // Load primary language. + stats := make(LanguageStatList, 0, len(repos)) + if err := db.GetEngine(ctx). + Where("`is_primary` = ? AND `language` != ?", true, "other"). + In("`repo_id`", repoIDs). + Find(&stats); err != nil { + return fmt.Errorf("find primary languages: %v", err) + } + stats.LoadAttributes() + for i := range repos { + for _, st := range stats { + if st.RepoID == repos[i].ID { + repos[i].PrimaryLanguage = st + break + } + } + } + + return nil +} + +// LoadAttributes loads the attributes for the given RepositoryList +func (repos RepositoryList) LoadAttributes() error { + return repos.loadAttributes(db.DefaultContext) +} + +// SearchRepoOptions holds the search options +type SearchRepoOptions struct { + db.ListOptions + Actor *user_model.User + Keyword string + OwnerID int64 + PriorityOwnerID int64 + TeamID int64 + OrderBy db.SearchOrderBy + Private bool // Include private repositories in results + StarredByID int64 + WatchedByID int64 + AllPublic bool // Include also all public repositories of users and public organisations + AllLimited bool // Include also all public repositories of limited organisations + // None -> include public and private + // True -> include just private + // False -> include just public + IsPrivate util.OptionalBool + // None -> include collaborative AND non-collaborative + // True -> include just collaborative + // False -> include just non-collaborative + Collaborate util.OptionalBool + // None -> include forks AND non-forks + // True -> include just forks + // False -> include just non-forks + Fork util.OptionalBool + // None -> include templates AND non-templates + // True -> include just templates + // False -> include just non-templates + Template util.OptionalBool + // None -> include mirrors AND non-mirrors + // True -> include just mirrors + // False -> include just non-mirrors + Mirror util.OptionalBool + // None -> include archived AND non-archived + // True -> include just archived + // False -> include just non-archived + Archived util.OptionalBool + // only search topic name + TopicOnly bool + // only search repositories with specified primary language + Language string + // include description in keyword search + IncludeDescription bool + // None -> include has milestones AND has no milestone + // True -> include just has milestones + // False -> include just has no milestone + HasMilestones util.OptionalBool + // LowerNames represents valid lower names to restrict to + LowerNames []string +} + +// SearchOrderBy is used to sort the result +type SearchOrderBy string + +func (s SearchOrderBy) String() string { + return string(s) +} + +// Strings for sorting result +const ( + SearchOrderByAlphabetically SearchOrderBy = "name ASC" + SearchOrderByAlphabeticallyReverse SearchOrderBy = "name DESC" + SearchOrderByLeastUpdated SearchOrderBy = "updated_unix ASC" + SearchOrderByRecentUpdated SearchOrderBy = "updated_unix DESC" + SearchOrderByOldest SearchOrderBy = "created_unix ASC" + SearchOrderByNewest SearchOrderBy = "created_unix DESC" + SearchOrderBySize SearchOrderBy = "size ASC" + SearchOrderBySizeReverse SearchOrderBy = "size DESC" + SearchOrderByID SearchOrderBy = "id ASC" + SearchOrderByIDReverse SearchOrderBy = "id DESC" + SearchOrderByStars SearchOrderBy = "num_stars ASC" + SearchOrderByStarsReverse SearchOrderBy = "num_stars DESC" + SearchOrderByForks SearchOrderBy = "num_forks ASC" + SearchOrderByForksReverse SearchOrderBy = "num_forks DESC" +) + +// UserOwnedRepoCond returns user ownered repositories +func UserOwnedRepoCond(userID int64) builder.Cond { + return builder.Eq{ + "repository.owner_id": userID, + } +} + +// UserAssignedRepoCond return user as assignee repositories list +func UserAssignedRepoCond(id string, userID int64) builder.Cond { + return builder.And( + builder.Eq{ + "repository.is_private": false, + }, + builder.In(id, + builder.Select("issue.repo_id").From("issue_assignees"). + InnerJoin("issue", "issue.id = issue_assignees.issue_id"). + Where(builder.Eq{ + "issue_assignees.assignee_id": userID, + }), + ), + ) +} + +// UserCreateIssueRepoCond return user created issues repositories list +func UserCreateIssueRepoCond(id string, userID int64, isPull bool) builder.Cond { + return builder.And( + builder.Eq{ + "repository.is_private": false, + }, + builder.In(id, + builder.Select("issue.repo_id").From("issue"). + Where(builder.Eq{ + "issue.poster_id": userID, + "issue.is_pull": isPull, + }), + ), + ) +} + +// UserMentionedRepoCond return user metinoed repositories list +func UserMentionedRepoCond(id string, userID int64) builder.Cond { + return builder.And( + builder.Eq{ + "repository.is_private": false, + }, + builder.In(id, + builder.Select("issue.repo_id").From("issue_user"). + InnerJoin("issue", "issue.id = issue_user.issue_id"). + Where(builder.Eq{ + "issue_user.is_mentioned": true, + "issue_user.uid": userID, + }), + ), + ) +} + +// UserCollaborationRepoCond returns user as collabrators repositories list +func UserCollaborationRepoCond(idStr string, userID int64) builder.Cond { + return builder.In(idStr, builder.Select("repo_id"). + From("`access`"). + Where(builder.And( + builder.Eq{"`access`.user_id": userID}, + builder.Gt{"`access`.mode": int(perm.AccessModeNone)}, + )), + ) +} + +// userOrgTeamRepoCond selects repos that the given user has access to through team membership +func userOrgTeamRepoCond(idStr string, userID int64) builder.Cond { + return builder.In(idStr, userOrgTeamRepoBuilder(userID)) +} + +// userOrgTeamRepoBuilder returns repo ids where user's teams can access. +func userOrgTeamRepoBuilder(userID int64) *builder.Builder { + return builder.Select("`team_repo`.repo_id"). + From("team_repo"). + Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id"). + Where(builder.Eq{"`team_user`.uid": userID}) +} + +// userOrgTeamUnitRepoBuilder returns repo ids where user's teams can access the special unit. +func userOrgTeamUnitRepoBuilder(userID int64, unitType unit.Type) *builder.Builder { + return userOrgTeamRepoBuilder(userID). + Join("INNER", "team_unit", "`team_unit`.team_id = `team_repo`.team_id"). + Where(builder.Eq{"`team_unit`.`type`": unitType}) +} + +// UserOrgUnitRepoCond selects repos that the given user has access to through org and the special unit +func UserOrgUnitRepoCond(idStr string, userID, orgID int64, unitType unit.Type) builder.Cond { + return builder.In(idStr, + userOrgTeamUnitRepoBuilder(userID, unitType). + And(builder.Eq{"`team_unit`.org_id": orgID}), + ) +} + +// userOrgPublicRepoCond returns the condition that one user could access all public repositories in organizations +func userOrgPublicRepoCond(userID int64) builder.Cond { + return builder.And( + builder.Eq{"`repository`.is_private": false}, + builder.In("`repository`.owner_id", + builder.Select("`org_user`.org_id"). + From("org_user"). + Where(builder.Eq{"`org_user`.uid": userID}), + ), + ) +} + +// userOrgPublicRepoCondPrivate returns the condition that one user could access all public repositories in private organizations +func userOrgPublicRepoCondPrivate(userID int64) builder.Cond { + return builder.And( + builder.Eq{"`repository`.is_private": false}, + builder.In("`repository`.owner_id", + builder.Select("`org_user`.org_id"). + From("org_user"). + Join("INNER", "`user`", "`user`.id = `org_user`.org_id"). + Where(builder.Eq{ + "`org_user`.uid": userID, + "`user`.`type`": user_model.UserTypeOrganization, + "`user`.visibility": structs.VisibleTypePrivate, + }), + ), + ) +} + +// UserOrgPublicUnitRepoCond returns the condition that one user could access all public repositories in the special organization +func UserOrgPublicUnitRepoCond(userID, orgID int64) builder.Cond { + return userOrgPublicRepoCond(userID). + And(builder.Eq{"`repository`.owner_id": orgID}) +} + +// SearchRepositoryCondition creates a query condition according search repository options +func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { + cond := builder.NewCond() + + if opts.Private { + if opts.Actor != nil && !opts.Actor.IsAdmin && opts.Actor.ID != opts.OwnerID { + // OK we're in the context of a User + cond = cond.And(AccessibleRepositoryCondition(opts.Actor)) + } + } else { + // Not looking at private organisations and users + // We should be able to see all non-private repositories that + // isn't in a private or limited organisation. + cond = cond.And( + builder.Eq{"is_private": false}, + builder.NotIn("owner_id", builder.Select("id").From("`user`").Where( + builder.Or(builder.Eq{"visibility": structs.VisibleTypeLimited}, builder.Eq{"visibility": structs.VisibleTypePrivate}), + ))) + } + + if opts.IsPrivate != util.OptionalBoolNone { + cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.IsTrue()}) + } + + if opts.Template != util.OptionalBoolNone { + cond = cond.And(builder.Eq{"is_template": opts.Template == util.OptionalBoolTrue}) + } + + // Restrict to starred repositories + if opts.StarredByID > 0 { + cond = cond.And(builder.In("id", builder.Select("repo_id").From("star").Where(builder.Eq{"uid": opts.StarredByID}))) + } + + // Restrict to watched repositories + if opts.WatchedByID > 0 { + cond = cond.And(builder.In("id", builder.Select("repo_id").From("watch").Where(builder.Eq{"user_id": opts.WatchedByID}))) + } + + // Restrict repositories to those the OwnerID owns or contributes to as per opts.Collaborate + if opts.OwnerID > 0 { + accessCond := builder.NewCond() + if opts.Collaborate != util.OptionalBoolTrue { + accessCond = builder.Eq{"owner_id": opts.OwnerID} + } + + if opts.Collaborate != util.OptionalBoolFalse { + // A Collaboration is: + collaborateCond := builder.And( + // 1. Repository we don't own + builder.Neq{"owner_id": opts.OwnerID}, + // 2. But we can see because of: + builder.Or( + // A. We have access + UserCollaborationRepoCond("`repository`.id", opts.OwnerID), + // B. We are in a team for + userOrgTeamRepoCond("`repository`.id", opts.OwnerID), + // C. Public repositories in organizations that we are member of + userOrgPublicRepoCondPrivate(opts.OwnerID), + ), + ) + if !opts.Private { + collaborateCond = collaborateCond.And(builder.Expr("owner_id NOT IN (SELECT org_id FROM org_user WHERE org_user.uid = ? AND org_user.is_public = ?)", opts.OwnerID, false)) + } + + accessCond = accessCond.Or(collaborateCond) + } + + if opts.AllPublic { + accessCond = accessCond.Or(builder.Eq{"is_private": false}.And(builder.In("owner_id", builder.Select("`user`.id").From("`user`").Where(builder.Eq{"`user`.visibility": structs.VisibleTypePublic})))) + } + + if opts.AllLimited { + accessCond = accessCond.Or(builder.Eq{"is_private": false}.And(builder.In("owner_id", builder.Select("`user`.id").From("`user`").Where(builder.Eq{"`user`.visibility": structs.VisibleTypeLimited})))) + } + + cond = cond.And(accessCond) + } + + if opts.TeamID > 0 { + cond = cond.And(builder.In("`repository`.id", builder.Select("`team_repo`.repo_id").From("team_repo").Where(builder.Eq{"`team_repo`.team_id": opts.TeamID}))) + } + + if opts.Keyword != "" { + // separate keyword + subQueryCond := builder.NewCond() + for _, v := range strings.Split(opts.Keyword, ",") { + if opts.TopicOnly { + subQueryCond = subQueryCond.Or(builder.Eq{"topic.name": strings.ToLower(v)}) + } else { + subQueryCond = subQueryCond.Or(builder.Like{"topic.name", strings.ToLower(v)}) + } + } + subQuery := builder.Select("repo_topic.repo_id").From("repo_topic"). + Join("INNER", "topic", "topic.id = repo_topic.topic_id"). + Where(subQueryCond). + GroupBy("repo_topic.repo_id") + + keywordCond := builder.In("id", subQuery) + if !opts.TopicOnly { + likes := builder.NewCond() + for _, v := range strings.Split(opts.Keyword, ",") { + likes = likes.Or(builder.Like{"lower_name", strings.ToLower(v)}) + + // If the string looks like "org/repo", match against that pattern too + if opts.TeamID == 0 && strings.Count(opts.Keyword, "/") == 1 { + pieces := strings.Split(opts.Keyword, "/") + ownerName := pieces[0] + repoName := pieces[1] + likes = likes.Or(builder.And(builder.Like{"owner_name", strings.ToLower(ownerName)}, builder.Like{"lower_name", strings.ToLower(repoName)})) + } + + if opts.IncludeDescription { + likes = likes.Or(builder.Like{"LOWER(description)", strings.ToLower(v)}) + } + } + keywordCond = keywordCond.Or(likes) + } + cond = cond.And(keywordCond) + } + + if opts.Language != "" { + cond = cond.And(builder.In("id", builder. + Select("repo_id"). + From("language_stat"). + Where(builder.Eq{"language": opts.Language}).And(builder.Eq{"is_primary": true}))) + } + + if opts.Fork != util.OptionalBoolNone { + cond = cond.And(builder.Eq{"is_fork": opts.Fork == util.OptionalBoolTrue}) + } + + if opts.Mirror != util.OptionalBoolNone { + cond = cond.And(builder.Eq{"is_mirror": opts.Mirror == util.OptionalBoolTrue}) + } + + if opts.Actor != nil && opts.Actor.IsRestricted { + cond = cond.And(AccessibleRepositoryCondition(opts.Actor)) + } + + if opts.Archived != util.OptionalBoolNone { + cond = cond.And(builder.Eq{"is_archived": opts.Archived == util.OptionalBoolTrue}) + } + + switch opts.HasMilestones { + case util.OptionalBoolTrue: + cond = cond.And(builder.Gt{"num_milestones": 0}) + case util.OptionalBoolFalse: + cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"})) + } + + return cond +} + +// SearchRepository returns repositories based on search options, +// it returns results in given range and number of total results. +func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) { + cond := SearchRepositoryCondition(opts) + return SearchRepositoryByCondition(opts, cond, true) +} + +// SearchRepositoryByCondition search repositories by condition +func SearchRepositoryByCondition(opts *SearchRepoOptions, cond builder.Cond, loadAttributes bool) (RepositoryList, int64, error) { + ctx := db.DefaultContext + sess, count, err := searchRepositoryByCondition(ctx, opts, cond) + if err != nil { + return nil, 0, err + } + + defaultSize := 50 + if opts.PageSize > 0 { + defaultSize = opts.PageSize + } + repos := make(RepositoryList, 0, defaultSize) + if err := sess.Find(&repos); err != nil { + return nil, 0, fmt.Errorf("Repo: %v", err) + } + + if opts.PageSize <= 0 { + count = int64(len(repos)) + } + + if loadAttributes { + if err := repos.loadAttributes(ctx); err != nil { + return nil, 0, fmt.Errorf("LoadAttributes: %v", err) + } + } + + return repos, count, nil +} + +func searchRepositoryByCondition(ctx context.Context, opts *SearchRepoOptions, cond builder.Cond) (db.Engine, int64, error) { + if opts.Page <= 0 { + opts.Page = 1 + } + + if len(opts.OrderBy) == 0 { + opts.OrderBy = db.SearchOrderByAlphabetically + } + + args := make([]interface{}, 0) + if opts.PriorityOwnerID > 0 { + opts.OrderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_id = ? THEN 0 ELSE owner_id END, %s", opts.OrderBy)) + args = append(args, opts.PriorityOwnerID) + } else if strings.Count(opts.Keyword, "/") == 1 { + // With "owner/repo" search times, prioritise results which match the owner field + orgName := strings.Split(opts.Keyword, "/")[0] + opts.OrderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_name LIKE ? THEN 0 ELSE 1 END, %s", opts.OrderBy)) + args = append(args, orgName) + } + + sess := db.GetEngine(ctx) + + var count int64 + if opts.PageSize > 0 { + var err error + count, err = sess. + Where(cond). + Count(new(Repository)) + if err != nil { + return nil, 0, fmt.Errorf("Count: %v", err) + } + } + + sess = sess.Where(cond).OrderBy(opts.OrderBy.String(), args...) + if opts.PageSize > 0 { + sess = sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) + } + return sess, count, nil +} + +// AccessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible +func AccessibleRepositoryCondition(user *user_model.User) builder.Cond { + cond := builder.NewCond() + + if user == nil || !user.IsRestricted || user.ID <= 0 { + orgVisibilityLimit := []structs.VisibleType{structs.VisibleTypePrivate} + if user == nil || user.ID <= 0 { + orgVisibilityLimit = append(orgVisibilityLimit, structs.VisibleTypeLimited) + } + // 1. Be able to see all non-private repositories that either: + cond = cond.Or(builder.And( + builder.Eq{"`repository`.is_private": false}, + // 2. Aren't in an private organisation or limited organisation if we're not logged in + builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where( + builder.And( + builder.Eq{"type": user_model.UserTypeOrganization}, + builder.In("visibility", orgVisibilityLimit)), + )))) + } + + if user != nil { + cond = cond.Or( + // 2. Be able to see all repositories that we have access to + UserCollaborationRepoCond("`repository`.id", user.ID), + // 3. Repositories that we directly own + builder.Eq{"`repository`.owner_id": user.ID}, + // 4. Be able to see all repositories that we are in a team + userOrgTeamRepoCond("`repository`.id", user.ID), + // 5. Be able to see all public repos in private organizations that we are an org_user of + userOrgPublicRepoCond(user.ID), + ) + } + + return cond +} + +// SearchRepositoryByName takes keyword and part of repository name to search, +// it returns results in given range and number of total results. +func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, error) { + opts.IncludeDescription = false + return SearchRepository(opts) +} + +// SearchRepositoryIDs takes keyword and part of repository name to search, +// it returns results in given range and number of total results. +func SearchRepositoryIDs(opts *SearchRepoOptions) ([]int64, int64, error) { + opts.IncludeDescription = false + + cond := SearchRepositoryCondition(opts) + + sess, count, err := searchRepositoryByCondition(db.DefaultContext, opts, cond) + if err != nil { + return nil, 0, err + } + + defaultSize := 50 + if opts.PageSize > 0 { + defaultSize = opts.PageSize + } + + ids := make([]int64, 0, defaultSize) + err = sess.Select("id").Table("repository").Find(&ids) + if opts.PageSize <= 0 { + count = int64(len(ids)) + } + + return ids, count, err +} + +// AccessibleRepoIDsQuery queries accessible repository ids. Usable as a subquery wherever repo ids need to be filtered. +func AccessibleRepoIDsQuery(user *user_model.User) *builder.Builder { + // NB: Please note this code needs to still work if user is nil + return builder.Select("id").From("repository").Where(AccessibleRepositoryCondition(user)) +} + +// FindUserAccessibleRepoIDs find all accessible repositories' ID by user's id +func FindUserAccessibleRepoIDs(user *user_model.User) ([]int64, error) { + repoIDs := make([]int64, 0, 10) + if err := db.GetEngine(db.DefaultContext). + Table("repository"). + Cols("id"). + Where(AccessibleRepositoryCondition(user)). + Find(&repoIDs); err != nil { + return nil, fmt.Errorf("FindUserAccesibleRepoIDs: %v", err) + } + return repoIDs, nil +} + +// GetUserRepositories returns a list of repositories of given user. +func GetUserRepositories(opts *SearchRepoOptions) (RepositoryList, int64, error) { + if len(opts.OrderBy) == 0 { + opts.OrderBy = "updated_unix DESC" + } + + cond := builder.NewCond() + cond = cond.And(builder.Eq{"owner_id": opts.Actor.ID}) + if !opts.Private { + cond = cond.And(builder.Eq{"is_private": false}) + } + + if opts.LowerNames != nil && len(opts.LowerNames) > 0 { + cond = cond.And(builder.In("lower_name", opts.LowerNames)) + } + + sess := db.GetEngine(db.DefaultContext) + + count, err := sess.Where(cond).Count(new(Repository)) + if err != nil { + return nil, 0, fmt.Errorf("Count: %v", err) + } + + sess = sess.Where(cond).OrderBy(opts.OrderBy.String()) + repos := make(RepositoryList, 0, opts.PageSize) + return repos, count, db.SetSessionPagination(sess, opts).Find(&repos) +} diff --git a/models/repo_list_test.go b/models/repo/repo_list_test.go similarity index 57% rename from models/repo_list_test.go rename to models/repo/repo_list_test.go index d45e10fb8..f9c84a0f3 100644 --- a/models/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -2,13 +2,14 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package repo_test import ( "strings" "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/util" @@ -19,7 +20,7 @@ func TestSearchRepository(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) // test search public repository on explore page - repos, count, err := SearchRepositoryByName(&SearchRepoOptions{ + repos, count, err := repo_model.SearchRepositoryByName(&repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -34,7 +35,7 @@ func TestSearchRepository(t *testing.T) { } assert.Equal(t, int64(1), count) - repos, count, err = SearchRepositoryByName(&SearchRepoOptions{ + repos, count, err = repo_model.SearchRepositoryByName(&repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -48,7 +49,7 @@ func TestSearchRepository(t *testing.T) { assert.Len(t, repos, 2) // test search private repository on explore page - repos, count, err = SearchRepositoryByName(&SearchRepoOptions{ + repos, count, err = repo_model.SearchRepositoryByName(&repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -64,7 +65,7 @@ func TestSearchRepository(t *testing.T) { } assert.Equal(t, int64(1), count) - repos, count, err = SearchRepositoryByName(&SearchRepoOptions{ + repos, count, err = repo_model.SearchRepositoryByName(&repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -79,14 +80,14 @@ func TestSearchRepository(t *testing.T) { assert.Len(t, repos, 3) // Test non existing owner - repos, count, err = SearchRepositoryByName(&SearchRepoOptions{OwnerID: unittest.NonexistentID}) + repos, count, err = repo_model.SearchRepositoryByName(&repo_model.SearchRepoOptions{OwnerID: unittest.NonexistentID}) assert.NoError(t, err) assert.Empty(t, repos) assert.Equal(t, int64(0), count) // Test search within description - repos, count, err = SearchRepository(&SearchRepoOptions{ + repos, count, err = repo_model.SearchRepository(&repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -103,7 +104,7 @@ func TestSearchRepository(t *testing.T) { assert.Equal(t, int64(1), count) // Test NOT search within description - repos, count, err = SearchRepository(&SearchRepoOptions{ + repos, count, err = repo_model.SearchRepository(&repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -119,164 +120,164 @@ func TestSearchRepository(t *testing.T) { testCases := []struct { name string - opts *SearchRepoOptions + opts *repo_model.SearchRepoOptions count int }{ { name: "PublicRepositoriesByName", - opts: &SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: util.OptionalBoolFalse}, count: 7, }, { name: "PublicAndPrivateRepositoriesByName", - opts: &SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: util.OptionalBoolFalse}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFirstPage", - opts: &SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitSecondPage", - opts: &SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitThirdPage", - opts: &SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFourthPage", - opts: &SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, count: 14, }, { name: "PublicRepositoriesOfUser", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: util.OptionalBoolFalse}, count: 2, }, { name: "PublicRepositoriesOfUser2", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: util.OptionalBoolFalse}, count: 0, }, { name: "PublicRepositoriesOfUser3", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: util.OptionalBoolFalse}, count: 2, }, { name: "PublicAndPrivateRepositoriesOfUser", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: util.OptionalBoolFalse}, count: 4, }, { name: "PublicAndPrivateRepositoriesOfUser2", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: util.OptionalBoolFalse}, count: 0, }, { name: "PublicAndPrivateRepositoriesOfUser3", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: util.OptionalBoolFalse}, count: 4, }, { name: "PublicRepositoriesOfUserIncludingCollaborative", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15}, count: 5, }, { name: "PublicRepositoriesOfUser2IncludingCollaborative", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18}, count: 1, }, { name: "PublicRepositoriesOfUser3IncludingCollaborative", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20}, count: 3, }, { name: "PublicAndPrivateRepositoriesOfUserIncludingCollaborative", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true}, count: 9, }, { name: "PublicAndPrivateRepositoriesOfUser2IncludingCollaborative", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true}, count: 4, }, { name: "PublicAndPrivateRepositoriesOfUser3IncludingCollaborative", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true}, count: 7, }, { name: "PublicRepositoriesOfOrganization", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: util.OptionalBoolFalse}, count: 1, }, { name: "PublicAndPrivateRepositoriesOfOrganization", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: util.OptionalBoolFalse}, count: 2, }, { name: "AllPublic/PublicRepositoriesByName", - opts: &SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: util.OptionalBoolFalse}, count: 7, }, { name: "AllPublic/PublicAndPrivateRepositoriesByName", - opts: &SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: util.OptionalBoolFalse}, count: 14, }, { name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse}, count: 28, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse}, count: 33, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", - opts: &SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true}, + opts: &repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true}, count: 15, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUser2IncludingCollaborativeByName", - opts: &SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, AllPublic: true}, + opts: &repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, AllPublic: true}, count: 13, }, { name: "AllPublic/PublicRepositoriesOfOrganization", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse}, count: 28, }, { name: "AllTemplates", - opts: &SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: util.OptionalBoolTrue}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: util.OptionalBoolTrue}, count: 2, }, { name: "OwnerSlashRepoSearch", - opts: &SearchRepoOptions{Keyword: "user/repo2", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0}, + opts: &repo_model.SearchRepoOptions{Keyword: "user/repo2", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0}, count: 3, }, { name: "OwnerSlashSearch", - opts: &SearchRepoOptions{Keyword: "user20/", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0}, + opts: &repo_model.SearchRepoOptions{Keyword: "user20/", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0}, count: 4, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - repos, count, err := SearchRepositoryByName(testCase.opts) + repos, count, err := repo_model.SearchRepositoryByName(testCase.opts) assert.NoError(t, err) assert.Equal(t, int64(testCase.count), count) @@ -354,29 +355,29 @@ func TestSearchRepositoryByTopicName(t *testing.T) { testCases := []struct { name string - opts *SearchRepoOptions + opts *repo_model.SearchRepoOptions count int }{ { name: "AllPublic/SearchPublicRepositoriesFromTopicAndName", - opts: &SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql"}, + opts: &repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql"}, count: 2, }, { name: "AllPublic/OnlySearchPublicRepositoriesFromTopic", - opts: &SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql", TopicOnly: true}, + opts: &repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql", TopicOnly: true}, count: 1, }, { name: "AllPublic/OnlySearchMultipleKeywordPublicRepositoriesFromTopic", - opts: &SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql,golang", TopicOnly: true}, + opts: &repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql,golang", TopicOnly: true}, count: 2, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - _, count, err := SearchRepositoryByName(testCase.opts) + _, count, err := repo_model.SearchRepositoryByName(testCase.opts) assert.NoError(t, err) assert.Equal(t, int64(testCase.count), count) }) diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index cf6ee8b67..8ae84eab5 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/util" @@ -15,18 +16,18 @@ import ( ) var ( - countRepospts = CountRepositoryOptions{OwnerID: 10} - countReposptsPublic = CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolFalse} - countReposptsPrivate = CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolTrue} + countRepospts = repo_model.CountRepositoryOptions{OwnerID: 10} + countReposptsPublic = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolFalse} + countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolTrue} ) func TestGetRepositoryCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) ctx := db.DefaultContext - count, err1 := CountRepositories(ctx, countRepospts) - privateCount, err2 := CountRepositories(ctx, countReposptsPrivate) - publicCount, err3 := CountRepositories(ctx, countReposptsPublic) + count, err1 := repo_model.CountRepositories(ctx, countRepospts) + privateCount, err2 := repo_model.CountRepositories(ctx, countReposptsPrivate) + publicCount, err3 := repo_model.CountRepositories(ctx, countReposptsPublic) assert.NoError(t, err1) assert.NoError(t, err2) assert.NoError(t, err3) @@ -37,7 +38,7 @@ func TestGetRepositoryCount(t *testing.T) { func TestGetPublicRepositoryCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - count, err := CountRepositories(db.DefaultContext, countReposptsPublic) + count, err := repo_model.CountRepositories(db.DefaultContext, countReposptsPublic) assert.NoError(t, err) assert.Equal(t, int64(1), count) } @@ -45,14 +46,14 @@ func TestGetPublicRepositoryCount(t *testing.T) { func TestGetPrivateRepositoryCount(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - count, err := CountRepositories(db.DefaultContext, countReposptsPrivate) + count, err := repo_model.CountRepositories(db.DefaultContext, countReposptsPrivate) 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) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}).(*repo_model.Repository) assert.Equal(t, "https://try.gitea.io/api/v1/repos/user12/repo10", repo.APIURL()) } diff --git a/models/repo/star_test.go b/models/repo/star_test.go index 2dde09c74..aa72b1dac 100644 --- a/models/repo/star_test.go +++ b/models/repo/star_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -17,26 +18,26 @@ 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}) + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) + assert.NoError(t, repo_model.StarRepo(userID, repoID, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) + assert.NoError(t, repo_model.StarRepo(userID, repoID, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) + assert.NoError(t, repo_model.StarRepo(userID, repoID, false)) + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) } func TestIsStaring(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - assert.True(t, IsStaring(db.DefaultContext, 2, 4)) - assert.False(t, IsStaring(db.DefaultContext, 3, 4)) + assert.True(t, repo_model.IsStaring(db.DefaultContext, 2, 4)) + assert.False(t, repo_model.IsStaring(db.DefaultContext, 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}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}).(*repo_model.Repository) + gazers, err := repo_model.GetStargazers(repo, db.ListOptions{Page: 0}) assert.NoError(t, err) if assert.Len(t, gazers, 1) { assert.Equal(t, int64(2), gazers[0].ID) @@ -46,8 +47,8 @@ func TestRepository_GetStargazers(t *testing.T) { 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}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository) + gazers, err := repo_model.GetStargazers(repo, db.ListOptions{Page: 0}) assert.NoError(t, err) assert.Len(t, gazers, 0) } diff --git a/models/repo/topic_test.go b/models/repo/topic_test.go index 353d96ef3..8187addb8 100644 --- a/models/repo/topic_test.go +++ b/models/repo/topic_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -19,47 +20,47 @@ func TestAddTopic(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - topics, _, err := FindTopics(&FindTopicOptions{}) + topics, _, err := repo_model.FindTopics(&repo_model.FindTopicOptions{}) assert.NoError(t, err) assert.Len(t, topics, totalNrOfTopics) - topics, total, err := FindTopics(&FindTopicOptions{ + topics, total, err := repo_model.FindTopics(&repo_model.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{ + topics, _, err = repo_model.FindTopics(&repo_model.FindTopicOptions{ RepoID: 1, }) assert.NoError(t, err) assert.Len(t, topics, repo1NrOfTopics) - assert.NoError(t, SaveTopics(2, "golang")) + assert.NoError(t, repo_model.SaveTopics(2, "golang")) repo2NrOfTopics := 1 - topics, _, err = FindTopics(&FindTopicOptions{}) + topics, _, err = repo_model.FindTopics(&repo_model.FindTopicOptions{}) assert.NoError(t, err) assert.Len(t, topics, totalNrOfTopics) - topics, _, err = FindTopics(&FindTopicOptions{ + topics, _, err = repo_model.FindTopics(&repo_model.FindTopicOptions{ RepoID: 2, }) assert.NoError(t, err) assert.Len(t, topics, repo2NrOfTopics) - assert.NoError(t, SaveTopics(2, "golang", "gitea")) + assert.NoError(t, repo_model.SaveTopics(2, "golang", "gitea")) repo2NrOfTopics = 2 totalNrOfTopics++ - topic, err := GetTopicByName("gitea") + topic, err := repo_model.GetTopicByName("gitea") assert.NoError(t, err) assert.EqualValues(t, 1, topic.RepoCount) - topics, _, err = FindTopics(&FindTopicOptions{}) + topics, _, err = repo_model.FindTopics(&repo_model.FindTopicOptions{}) assert.NoError(t, err) assert.Len(t, topics, totalNrOfTopics) - topics, _, err = FindTopics(&FindTopicOptions{ + topics, _, err = repo_model.FindTopics(&repo_model.FindTopicOptions{ RepoID: 2, }) assert.NoError(t, err) @@ -67,14 +68,14 @@ func TestAddTopic(t *testing.T) { } 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.True(t, repo_model.ValidateTopic("12345")) + assert.True(t, repo_model.ValidateTopic("2-test")) + assert.True(t, repo_model.ValidateTopic("test-3")) + assert.True(t, repo_model.ValidateTopic("first")) + assert.True(t, repo_model.ValidateTopic("second-test-topic")) + assert.True(t, repo_model.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")) + assert.False(t, repo_model.ValidateTopic("$fourth-test,topic")) + assert.False(t, repo_model.ValidateTopic("-fifth-test-topic")) + assert.False(t, repo_model.ValidateTopic("sixth-go-project-topic-with-excess-length")) } diff --git a/models/repo/update.go b/models/repo/update.go index 7fb51c959..07776ebc0 100644 --- a/models/repo/update.go +++ b/models/repo/update.go @@ -172,3 +172,11 @@ func ChangeRepositoryName(doer *user_model.User, repo *Repository, newRepoName s return committer.Commit() } + +// UpdateRepoSize updates the repository size, calculating it using util.GetDirectorySize +func UpdateRepoSize(ctx context.Context, repoID, size int64) error { + _, err := db.GetEngine(ctx).ID(repoID).Cols("size").NoAutoTime().Update(&Repository{ + Size: size, + }) + return err +} diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index fe9677179..e697505b8 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -5,7 +5,14 @@ package repo import ( + "context" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + + "xorm.io/builder" ) // GetStarredRepos returns the repos starred by a particular user @@ -48,3 +55,118 @@ func GetWatchedRepos(userID int64, private bool, listOptions db.ListOptions) ([] total, err := sess.FindAndCount(&repos) return repos, total, err } + +// GetRepoAssignees returns all users that have write access and can be assigned to issues +// of the repository, +func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.User, err error) { + if err = repo.GetOwner(ctx); err != nil { + return nil, err + } + + e := db.GetEngine(ctx) + userIDs := make([]int64, 0, 10) + if err = e.Table("access"). + Where("repo_id = ? AND mode >= ?", repo.ID, perm.AccessModeWrite). + Select("user_id"). + Find(&userIDs); err != nil { + return nil, err + } + + additionalUserIDs := make([]int64, 0, 10) + if err = e.Table("team_user"). + Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). + Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id"). + Where("`team_repo`.repo_id = ? AND `team_unit`.access_mode >= ?", repo.ID, perm.AccessModeWrite). + Distinct("`team_user`.uid"). + Select("`team_user`.uid"). + Find(&additionalUserIDs); err != nil { + return nil, err + } + + uidMap := map[int64]bool{} + i := 0 + for _, uid := range userIDs { + if uidMap[uid] { + continue + } + uidMap[uid] = true + userIDs[i] = uid + i++ + } + userIDs = userIDs[:i] + userIDs = append(userIDs, additionalUserIDs...) + + for _, uid := range additionalUserIDs { + if uidMap[uid] { + continue + } + userIDs[i] = uid + i++ + } + userIDs = userIDs[:i] + + // Leave a seat for owner itself to append later, but if owner is an organization + // and just waste 1 unit is cheaper than re-allocate memory once. + users := make([]*user_model.User, 0, len(userIDs)+1) + if len(userIDs) > 0 { + if err = e.In("id", userIDs).Find(&users); err != nil { + return nil, err + } + } + if !repo.Owner.IsOrganization() && !uidMap[repo.OwnerID] { + users = append(users, repo.Owner) + } + + return users, nil +} + +// GetReviewers get all users can be requested to review: +// * for private repositories this returns all users that have read access or higher to the repository. +// * for public repositories this returns all users that have read access or higher to the repository, +// all repo watchers and all organization members. +// TODO: may be we should have a busy choice for users to block review request to them. +func GetReviewers(ctx context.Context, repo *Repository, doerID, posterID int64) ([]*user_model.User, error) { + // Get the owner of the repository - this often already pre-cached and if so saves complexity for the following queries + if err := repo.GetOwner(ctx); err != nil { + return nil, err + } + + cond := builder.And(builder.Neq{"`user`.id": posterID}) + + if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate { + // This a private repository: + // Anyone who can read the repository is a requestable reviewer + + cond = cond.And(builder.In("`user`.id", + builder.Select("user_id").From("access").Where( + builder.Eq{"repo_id": repo.ID}. + And(builder.Gte{"mode": perm.AccessModeRead}), + ), + )) + + if repo.Owner.Type == user_model.UserTypeIndividual && repo.Owner.ID != posterID { + // as private *user* repos don't generate an entry in the `access` table, + // the owner of a private repo needs to be explicitly added. + cond = cond.Or(builder.Eq{"`user`.id": repo.Owner.ID}) + } + + } else { + // This is a "public" repository: + // Any user that has read access, is a watcher or organization member can be requested to review + cond = cond.And(builder.And(builder.In("`user`.id", + builder.Select("user_id").From("access"). + Where(builder.Eq{"repo_id": repo.ID}. + And(builder.Gte{"mode": perm.AccessModeRead})), + ).Or(builder.In("`user`.id", + builder.Select("user_id").From("watch"). + Where(builder.Eq{"repo_id": repo.ID}. + And(builder.In("mode", WatchModeNormal, WatchModeAuto))), + ).Or(builder.In("`user`.id", + builder.Select("uid").From("org_user"). + Where(builder.Eq{"org_id": repo.OwnerID}), + ))))) + } + + users := make([]*user_model.User, 0, 8) + return users, db.GetEngine(ctx).Where(cond).OrderBy("name").Find(&users) +} diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go new file mode 100644 index 000000000..d024729b9 --- /dev/null +++ b/models/repo/user_repo_test.go @@ -0,0 +1,74 @@ +// 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_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestRepoAssignees(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) + users, err := repo_model.GetRepoAssignees(db.DefaultContext, repo2) + assert.NoError(t, err) + assert.Len(t, users, 1) + assert.Equal(t, users[0].ID, int64(2)) + + repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21}).(*repo_model.Repository) + users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21) + assert.NoError(t, err) + assert.Len(t, users, 3) + assert.Equal(t, users[0].ID, int64(15)) + assert.Equal(t, users[1].ID, int64(18)) + assert.Equal(t, users[2].ID, int64(16)) +} + +func TestRepoGetReviewers(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // test public repo + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + + ctx := db.DefaultContext + reviewers, err := repo_model.GetReviewers(ctx, repo1, 2, 2) + assert.NoError(t, err) + assert.Len(t, reviewers, 4) + + // should include doer if doer is not PR poster. + reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 2) + assert.NoError(t, err) + assert.Len(t, reviewers, 4) + + // should not include PR poster, if PR poster would be otherwise eligible + reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 4) + assert.NoError(t, err) + assert.Len(t, reviewers, 3) + + // test private user repo + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) + + reviewers, err = repo_model.GetReviewers(ctx, repo2, 2, 4) + assert.NoError(t, err) + assert.Len(t, reviewers, 1) + assert.EqualValues(t, reviewers[0].ID, 2) + + // test private org repo + repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository) + + reviewers, err = repo_model.GetReviewers(ctx, repo3, 2, 1) + assert.NoError(t, err) + assert.Len(t, reviewers, 2) + + reviewers, err = repo_model.GetReviewers(ctx, repo3, 2, 2) + assert.NoError(t, err) + assert.Len(t, reviewers, 1) +} diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go index 2f4e04ab1..3875e63fd 100644 --- a/models/repo/watch_test.go +++ b/models/repo/watch_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "testing" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" @@ -17,20 +18,20 @@ import ( 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.True(t, repo_model.IsWatching(1, 1)) + assert.True(t, repo_model.IsWatching(4, 1)) + assert.True(t, repo_model.IsWatching(11, 1)) - assert.False(t, IsWatching(1, 5)) - assert.False(t, IsWatching(8, 1)) - assert.False(t, IsWatching(unittest.NonexistentID, unittest.NonexistentID)) + assert.False(t, repo_model.IsWatching(1, 5)) + assert.False(t, repo_model.IsWatching(8, 1)) + assert.False(t, repo_model.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) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + watches, err := repo_model.GetWatchers(db.DefaultContext, repo.ID) assert.NoError(t, err) // One watchers are inactive, thus minus 1 assert.Len(t, watches, repo.NumWatches-1) @@ -38,7 +39,7 @@ func TestGetWatchers(t *testing.T) { assert.EqualValues(t, repo.ID, watch.RepoID) } - watches, err = GetWatchers(db.DefaultContext, unittest.NonexistentID) + watches, err = repo_model.GetWatchers(db.DefaultContext, unittest.NonexistentID) assert.NoError(t, err) assert.Len(t, watches, 0) } @@ -46,16 +47,16 @@ func TestGetWatchers(t *testing.T) { 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}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + watchers, err := repo_model.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}) + unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{UserID: watcher.ID, RepoID: repo.ID}) } - repo = unittest.AssertExistsAndLoadBean(t, &Repository{ID: 9}).(*Repository) - watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 9}).(*repo_model.Repository) + watchers, err = repo_model.GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, 0) } @@ -63,8 +64,8 @@ func TestRepository_GetWatchers(t *testing.T) { 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}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + watchers, err := repo_model.GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, repo.NumWatches) @@ -73,46 +74,46 @@ func TestWatchIfAuto(t *testing.T) { prevCount := repo.NumWatches // Must not add watch - assert.NoError(t, WatchIfAuto(db.DefaultContext, 8, 1, true)) - watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 8, 1, true)) + watchers, err = repo_model.GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, prevCount) // Should not add watch - assert.NoError(t, WatchIfAuto(db.DefaultContext, 10, 1, true)) - watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 10, 1, true)) + watchers, err = repo_model.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(db.DefaultContext, 8, 1, true)) - watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 8, 1, true)) + watchers, err = repo_model.GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, prevCount) // Should not add watch - assert.NoError(t, WatchIfAuto(db.DefaultContext, 12, 1, false)) - watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 12, 1, false)) + watchers, err = repo_model.GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, prevCount) // Should add watch - assert.NoError(t, WatchIfAuto(db.DefaultContext, 12, 1, true)) - watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 12, 1, true)) + watchers, err = repo_model.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(db.DefaultContext, 12, 1, false)) - watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, 12, 1, false)) + watchers, err = repo_model.GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, prevCount) // Must not add watch - assert.NoError(t, WatchIfAuto(db.DefaultContext, 12, 1, true)) - watchers, err = GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) + assert.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 12, 1, true)) + watchers, err = repo_model.GetRepoWatchers(repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, prevCount) } @@ -120,20 +121,20 @@ func TestWatchIfAuto(t *testing.T) { func TestWatchRepoMode(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 0) + unittest.AssertCount(t, &repo_model.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, repo_model.WatchRepoMode(12, 1, repo_model.WatchModeAuto)) + unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1) + unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.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, repo_model.WatchRepoMode(12, 1, repo_model.WatchModeNormal)) + unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1) + unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.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, repo_model.WatchRepoMode(12, 1, repo_model.WatchModeDont)) + unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1) + unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeDont}, 1) - assert.NoError(t, WatchRepoMode(12, 1, WatchModeNone)) - unittest.AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 0) + assert.NoError(t, repo_model.WatchRepoMode(12, 1, repo_model.WatchModeNone)) + unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0) } diff --git a/models/repo/wiki_test.go b/models/repo/wiki_test.go index f5e61e5ae..339289e05 100644 --- a/models/repo/wiki_test.go +++ b/models/repo/wiki_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package repo +package repo_test import ( "path/filepath" "testing" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" @@ -17,7 +18,7 @@ import ( func TestRepository_WikiCloneLink(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) cloneLink := repo.WikiCloneLink() assert.Equal(t, "ssh://sshuser@try.gitea.io:3000/user2/repo1.wiki.git", cloneLink.SSH) assert.Equal(t, "https://try.gitea.io/user2/repo1.wiki.git", cloneLink.HTTPS) @@ -26,20 +27,20 @@ func TestRepository_WikiCloneLink(t *testing.T) { func TestWikiPath(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) expected := filepath.Join(setting.RepoRootPath, "user2/repo1.wiki.git") - assert.Equal(t, expected, WikiPath("user2", "repo1")) + assert.Equal(t, expected, repo_model.WikiPath("user2", "repo1")) } func TestRepository_WikiPath(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) expected := filepath.Join(setting.RepoRootPath, "user2/repo1.wiki.git") assert.Equal(t, expected, repo.WikiPath()) } func TestRepository_HasWiki(t *testing.T) { unittest.PrepareTestEnv(t) - repo1 := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) assert.True(t, repo1.HasWiki()) - repo2 := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository) + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) assert.False(t, repo2.HasWiki()) } diff --git a/models/repo_generate.go b/models/repo_generate.go deleted file mode 100644 index 6b720b496..000000000 --- a/models/repo_generate.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2019 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 models - -import ( - "bufio" - "bytes" - "context" - "strings" - - "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/webhook" - "code.gitea.io/gitea/modules/log" - - "github.com/gobwas/glob" -) - -// GenerateRepoOptions contains the template units to generate -type GenerateRepoOptions struct { - Name string - DefaultBranch string - Description string - Private bool - GitContent bool - Topics bool - GitHooks bool - Webhooks bool - Avatar bool - IssueLabels bool -} - -// IsValid checks whether at least one option is chosen for generation -func (gro GenerateRepoOptions) IsValid() bool { - return gro.GitContent || gro.Topics || gro.GitHooks || gro.Webhooks || gro.Avatar || gro.IssueLabels // or other items as they are added -} - -// GiteaTemplate holds information about a .gitea/template file -type GiteaTemplate struct { - Path string - Content []byte - - globs []glob.Glob -} - -// Globs parses the .gitea/template globs or returns them if they were already parsed -func (gt GiteaTemplate) Globs() []glob.Glob { - if gt.globs != nil { - return gt.globs - } - - gt.globs = make([]glob.Glob, 0) - scanner := bufio.NewScanner(bytes.NewReader(gt.Content)) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - g, err := glob.Compile(line, '/') - if err != nil { - log.Info("Invalid glob expression '%s' (skipped): %v", line, err) - continue - } - gt.globs = append(gt.globs, g) - } - return gt.globs -} - -// GenerateWebhooks generates webhooks from a template repository -func GenerateWebhooks(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) error { - templateWebhooks, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{RepoID: templateRepo.ID}) - if err != nil { - return err - } - - for _, templateWebhook := range templateWebhooks { - generateWebhook := &webhook.Webhook{ - RepoID: generateRepo.ID, - URL: templateWebhook.URL, - HTTPMethod: templateWebhook.HTTPMethod, - ContentType: templateWebhook.ContentType, - Secret: templateWebhook.Secret, - HookEvent: templateWebhook.HookEvent, - IsActive: templateWebhook.IsActive, - Type: templateWebhook.Type, - OrgID: templateWebhook.OrgID, - Events: templateWebhook.Events, - Meta: templateWebhook.Meta, - } - if err := webhook.CreateWebhook(ctx, generateWebhook); err != nil { - return err - } - } - return nil -} - -// GenerateIssueLabels generates issue labels from a template repository -func GenerateIssueLabels(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) error { - templateLabels, err := GetLabelsByRepoID(ctx, templateRepo.ID, "", db.ListOptions{}) - if err != nil { - return err - } - - for _, templateLabel := range templateLabels { - generateLabel := &Label{ - RepoID: generateRepo.ID, - Name: templateLabel.Name, - Description: templateLabel.Description, - Color: templateLabel.Color, - } - if err := db.Insert(ctx, generateLabel); err != nil { - return err - } - } - return nil -} diff --git a/models/repo_list.go b/models/repo_list.go deleted file mode 100644 index 45fb10c36..000000000 --- a/models/repo_list.go +++ /dev/null @@ -1,704 +0,0 @@ -// 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 models - -import ( - "context" - "fmt" - "strings" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/organization" - "code.gitea.io/gitea/models/perm" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/container" - "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" - - "xorm.io/builder" -) - -// RepositoryListDefaultPageSize is the default number of repositories -// to load in memory when running administrative tasks on all (or almost -// all) of them. -// The number should be low enough to avoid filling up all RAM with -// repository data... -const RepositoryListDefaultPageSize = 64 - -// RepositoryList contains a list of repositories -type RepositoryList []*repo_model.Repository - -func (repos RepositoryList) Len() int { - return len(repos) -} - -func (repos RepositoryList) Less(i, j int) bool { - return repos[i].FullName() < repos[j].FullName() -} - -func (repos RepositoryList) Swap(i, j int) { - repos[i], repos[j] = repos[j], repos[i] -} - -// FIXME: Remove in favor of maps.values when MIN_GO_VERSION >= 1.18 -func valuesRepository(m map[int64]*repo_model.Repository) []*repo_model.Repository { - values := make([]*repo_model.Repository, 0, len(m)) - for _, v := range m { - values = append(values, v) - } - return values -} - -// RepositoryListOfMap make list from values of map -func RepositoryListOfMap(repoMap map[int64]*repo_model.Repository) RepositoryList { - return RepositoryList(valuesRepository(repoMap)) -} - -func (repos RepositoryList) loadAttributes(ctx context.Context) error { - if len(repos) == 0 { - return nil - } - - set := make(map[int64]struct{}) - repoIDs := make([]int64, len(repos)) - for i := range repos { - set[repos[i].OwnerID] = struct{}{} - repoIDs[i] = repos[i].ID - } - - // Load owners. - users := make(map[int64]*user_model.User, len(set)) - if err := db.GetEngine(ctx). - Where("id > 0"). - In("id", container.KeysInt64(set)). - Find(&users); err != nil { - return fmt.Errorf("find users: %v", err) - } - for i := range repos { - repos[i].Owner = users[repos[i].OwnerID] - } - - // Load primary language. - stats := make(repo_model.LanguageStatList, 0, len(repos)) - if err := db.GetEngine(ctx). - Where("`is_primary` = ? AND `language` != ?", true, "other"). - In("`repo_id`", repoIDs). - Find(&stats); err != nil { - return fmt.Errorf("find primary languages: %v", err) - } - stats.LoadAttributes() - for i := range repos { - for _, st := range stats { - if st.RepoID == repos[i].ID { - repos[i].PrimaryLanguage = st - break - } - } - } - - return nil -} - -// LoadAttributes loads the attributes for the given RepositoryList -func (repos RepositoryList) LoadAttributes() error { - return repos.loadAttributes(db.DefaultContext) -} - -// SearchRepoOptions holds the search options -type SearchRepoOptions struct { - db.ListOptions - Actor *user_model.User - Keyword string - OwnerID int64 - PriorityOwnerID int64 - TeamID int64 - OrderBy db.SearchOrderBy - Private bool // Include private repositories in results - StarredByID int64 - WatchedByID int64 - AllPublic bool // Include also all public repositories of users and public organisations - AllLimited bool // Include also all public repositories of limited organisations - // None -> include public and private - // True -> include just private - // False -> include just public - IsPrivate util.OptionalBool - // None -> include collaborative AND non-collaborative - // True -> include just collaborative - // False -> include just non-collaborative - Collaborate util.OptionalBool - // None -> include forks AND non-forks - // True -> include just forks - // False -> include just non-forks - Fork util.OptionalBool - // None -> include templates AND non-templates - // True -> include just templates - // False -> include just non-templates - Template util.OptionalBool - // None -> include mirrors AND non-mirrors - // True -> include just mirrors - // False -> include just non-mirrors - Mirror util.OptionalBool - // None -> include archived AND non-archived - // True -> include just archived - // False -> include just non-archived - Archived util.OptionalBool - // only search topic name - TopicOnly bool - // only search repositories with specified primary language - Language string - // include description in keyword search - IncludeDescription bool - // None -> include has milestones AND has no milestone - // True -> include just has milestones - // False -> include just has no milestone - HasMilestones util.OptionalBool - // LowerNames represents valid lower names to restrict to - LowerNames []string -} - -// SearchOrderBy is used to sort the result -type SearchOrderBy string - -func (s SearchOrderBy) String() string { - return string(s) -} - -// Strings for sorting result -const ( - SearchOrderByAlphabetically SearchOrderBy = "name ASC" - SearchOrderByAlphabeticallyReverse SearchOrderBy = "name DESC" - SearchOrderByLeastUpdated SearchOrderBy = "updated_unix ASC" - SearchOrderByRecentUpdated SearchOrderBy = "updated_unix DESC" - SearchOrderByOldest SearchOrderBy = "created_unix ASC" - SearchOrderByNewest SearchOrderBy = "created_unix DESC" - SearchOrderBySize SearchOrderBy = "size ASC" - SearchOrderBySizeReverse SearchOrderBy = "size DESC" - SearchOrderByID SearchOrderBy = "id ASC" - SearchOrderByIDReverse SearchOrderBy = "id DESC" - SearchOrderByStars SearchOrderBy = "num_stars ASC" - SearchOrderByStarsReverse SearchOrderBy = "num_stars DESC" - SearchOrderByForks SearchOrderBy = "num_forks ASC" - SearchOrderByForksReverse SearchOrderBy = "num_forks DESC" -) - -// userOwnedRepoCond returns user ownered repositories -func userOwnedRepoCond(userID int64) builder.Cond { - return builder.Eq{ - "repository.owner_id": userID, - } -} - -// userAssignedRepoCond return user as assignee repositories list -func userAssignedRepoCond(id string, userID int64) builder.Cond { - return builder.And( - builder.Eq{ - "repository.is_private": false, - }, - builder.In(id, - builder.Select("issue.repo_id").From("issue_assignees"). - InnerJoin("issue", "issue.id = issue_assignees.issue_id"). - Where(builder.Eq{ - "issue_assignees.assignee_id": userID, - }), - ), - ) -} - -// userCreateIssueRepoCond return user created issues repositories list -func userCreateIssueRepoCond(id string, userID int64, isPull bool) builder.Cond { - return builder.And( - builder.Eq{ - "repository.is_private": false, - }, - builder.In(id, - builder.Select("issue.repo_id").From("issue"). - Where(builder.Eq{ - "issue.poster_id": userID, - "issue.is_pull": isPull, - }), - ), - ) -} - -// userMentionedRepoCond return user metinoed repositories list -func userMentionedRepoCond(id string, userID int64) builder.Cond { - return builder.And( - builder.Eq{ - "repository.is_private": false, - }, - builder.In(id, - builder.Select("issue.repo_id").From("issue_user"). - InnerJoin("issue", "issue.id = issue_user.issue_id"). - Where(builder.Eq{ - "issue_user.is_mentioned": true, - "issue_user.uid": userID, - }), - ), - ) -} - -// teamUnitsRepoCond returns query condition for those repo id in the special org team with special units access -func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Type) builder.Cond { - return builder.In(id, - builder.Select("repo_id").From("team_repo").Where( - builder.Eq{ - "team_id": teamID, - }.And( - builder.Or( - // Check if the user is member of the team. - builder.In( - "team_id", builder.Select("team_id").From("team_user").Where( - builder.Eq{ - "uid": userID, - }, - ), - ), - // Check if the user is in the owner team of the organisation. - builder.Exists(builder.Select("team_id").From("team_user"). - Where(builder.Eq{ - "org_id": orgID, - "team_id": builder.Select("id").From("team").Where( - builder.Eq{ - "org_id": orgID, - "lower_name": strings.ToLower(organization.OwnerTeamName), - }), - "uid": userID, - }), - ), - )).And( - builder.In( - "team_id", builder.Select("team_id").From("team_unit").Where( - builder.Eq{ - "`team_unit`.org_id": orgID, - }.And( - builder.In("`team_unit`.type", units), - ), - ), - ), - ), - )) -} - -// userCollaborationRepoCond returns user as collabrators repositories list -func userCollaborationRepoCond(idStr string, userID int64) builder.Cond { - return builder.In(idStr, builder.Select("repo_id"). - From("`access`"). - Where(builder.And( - builder.Eq{"`access`.user_id": userID}, - builder.Gt{"`access`.mode": int(perm.AccessModeNone)}, - )), - ) -} - -// userOrgTeamRepoCond selects repos that the given user has access to through team membership -func userOrgTeamRepoCond(idStr string, userID int64) builder.Cond { - return builder.In(idStr, userOrgTeamRepoBuilder(userID)) -} - -// userOrgTeamRepoBuilder returns repo ids where user's teams can access. -func userOrgTeamRepoBuilder(userID int64) *builder.Builder { - return builder.Select("`team_repo`.repo_id"). - From("team_repo"). - Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id"). - Where(builder.Eq{"`team_user`.uid": userID}) -} - -// userOrgTeamUnitRepoBuilder returns repo ids where user's teams can access the special unit. -func userOrgTeamUnitRepoBuilder(userID int64, unitType unit.Type) *builder.Builder { - return userOrgTeamRepoBuilder(userID). - Join("INNER", "team_unit", "`team_unit`.team_id = `team_repo`.team_id"). - Where(builder.Eq{"`team_unit`.`type`": unitType}) -} - -// userOrgUnitRepoCond selects repos that the given user has access to through org and the special unit -func userOrgUnitRepoCond(idStr string, userID, orgID int64, unitType unit.Type) builder.Cond { - return builder.In(idStr, - userOrgTeamUnitRepoBuilder(userID, unitType). - And(builder.Eq{"`team_unit`.org_id": orgID}), - ) -} - -// userOrgPublicRepoCond returns the condition that one user could access all public repositories in organizations -func userOrgPublicRepoCond(userID int64) builder.Cond { - return builder.And( - builder.Eq{"`repository`.is_private": false}, - builder.In("`repository`.owner_id", - builder.Select("`org_user`.org_id"). - From("org_user"). - Where(builder.Eq{"`org_user`.uid": userID}), - ), - ) -} - -// userOrgPublicRepoCondPrivate returns the condition that one user could access all public repositories in private organizations -func userOrgPublicRepoCondPrivate(userID int64) builder.Cond { - return builder.And( - builder.Eq{"`repository`.is_private": false}, - builder.In("`repository`.owner_id", - builder.Select("`org_user`.org_id"). - From("org_user"). - Join("INNER", "`user`", "`user`.id = `org_user`.org_id"). - Where(builder.Eq{ - "`org_user`.uid": userID, - "`user`.`type`": user_model.UserTypeOrganization, - "`user`.visibility": structs.VisibleTypePrivate, - }), - ), - ) -} - -// userOrgPublicUnitRepoCond returns the condition that one user could access all public repositories in the special organization -func userOrgPublicUnitRepoCond(userID, orgID int64) builder.Cond { - return userOrgPublicRepoCond(userID). - And(builder.Eq{"`repository`.owner_id": orgID}) -} - -// SearchRepositoryCondition creates a query condition according search repository options -func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { - cond := builder.NewCond() - - if opts.Private { - if opts.Actor != nil && !opts.Actor.IsAdmin && opts.Actor.ID != opts.OwnerID { - // OK we're in the context of a User - cond = cond.And(accessibleRepositoryCondition(opts.Actor)) - } - } else { - // Not looking at private organisations and users - // We should be able to see all non-private repositories that - // isn't in a private or limited organisation. - cond = cond.And( - builder.Eq{"is_private": false}, - builder.NotIn("owner_id", builder.Select("id").From("`user`").Where( - builder.Or(builder.Eq{"visibility": structs.VisibleTypeLimited}, builder.Eq{"visibility": structs.VisibleTypePrivate}), - ))) - } - - if opts.IsPrivate != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.IsTrue()}) - } - - if opts.Template != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"is_template": opts.Template == util.OptionalBoolTrue}) - } - - // Restrict to starred repositories - if opts.StarredByID > 0 { - cond = cond.And(builder.In("id", builder.Select("repo_id").From("star").Where(builder.Eq{"uid": opts.StarredByID}))) - } - - // Restrict to watched repositories - if opts.WatchedByID > 0 { - cond = cond.And(builder.In("id", builder.Select("repo_id").From("watch").Where(builder.Eq{"user_id": opts.WatchedByID}))) - } - - // Restrict repositories to those the OwnerID owns or contributes to as per opts.Collaborate - if opts.OwnerID > 0 { - accessCond := builder.NewCond() - if opts.Collaborate != util.OptionalBoolTrue { - accessCond = builder.Eq{"owner_id": opts.OwnerID} - } - - if opts.Collaborate != util.OptionalBoolFalse { - // A Collaboration is: - collaborateCond := builder.And( - // 1. Repository we don't own - builder.Neq{"owner_id": opts.OwnerID}, - // 2. But we can see because of: - builder.Or( - // A. We have access - userCollaborationRepoCond("`repository`.id", opts.OwnerID), - // B. We are in a team for - userOrgTeamRepoCond("`repository`.id", opts.OwnerID), - // C. Public repositories in organizations that we are member of - userOrgPublicRepoCondPrivate(opts.OwnerID), - ), - ) - if !opts.Private { - collaborateCond = collaborateCond.And(builder.Expr("owner_id NOT IN (SELECT org_id FROM org_user WHERE org_user.uid = ? AND org_user.is_public = ?)", opts.OwnerID, false)) - } - - accessCond = accessCond.Or(collaborateCond) - } - - if opts.AllPublic { - accessCond = accessCond.Or(builder.Eq{"is_private": false}.And(builder.In("owner_id", builder.Select("`user`.id").From("`user`").Where(builder.Eq{"`user`.visibility": structs.VisibleTypePublic})))) - } - - if opts.AllLimited { - accessCond = accessCond.Or(builder.Eq{"is_private": false}.And(builder.In("owner_id", builder.Select("`user`.id").From("`user`").Where(builder.Eq{"`user`.visibility": structs.VisibleTypeLimited})))) - } - - cond = cond.And(accessCond) - } - - if opts.TeamID > 0 { - cond = cond.And(builder.In("`repository`.id", builder.Select("`team_repo`.repo_id").From("team_repo").Where(builder.Eq{"`team_repo`.team_id": opts.TeamID}))) - } - - if opts.Keyword != "" { - // separate keyword - subQueryCond := builder.NewCond() - for _, v := range strings.Split(opts.Keyword, ",") { - if opts.TopicOnly { - subQueryCond = subQueryCond.Or(builder.Eq{"topic.name": strings.ToLower(v)}) - } else { - subQueryCond = subQueryCond.Or(builder.Like{"topic.name", strings.ToLower(v)}) - } - } - subQuery := builder.Select("repo_topic.repo_id").From("repo_topic"). - Join("INNER", "topic", "topic.id = repo_topic.topic_id"). - Where(subQueryCond). - GroupBy("repo_topic.repo_id") - - keywordCond := builder.In("id", subQuery) - if !opts.TopicOnly { - likes := builder.NewCond() - for _, v := range strings.Split(opts.Keyword, ",") { - likes = likes.Or(builder.Like{"lower_name", strings.ToLower(v)}) - - // If the string looks like "org/repo", match against that pattern too - if opts.TeamID == 0 && strings.Count(opts.Keyword, "/") == 1 { - pieces := strings.Split(opts.Keyword, "/") - ownerName := pieces[0] - repoName := pieces[1] - likes = likes.Or(builder.And(builder.Like{"owner_name", strings.ToLower(ownerName)}, builder.Like{"lower_name", strings.ToLower(repoName)})) - } - - if opts.IncludeDescription { - likes = likes.Or(builder.Like{"LOWER(description)", strings.ToLower(v)}) - } - } - keywordCond = keywordCond.Or(likes) - } - cond = cond.And(keywordCond) - } - - if opts.Language != "" { - cond = cond.And(builder.In("id", builder. - Select("repo_id"). - From("language_stat"). - Where(builder.Eq{"language": opts.Language}).And(builder.Eq{"is_primary": true}))) - } - - if opts.Fork != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"is_fork": opts.Fork == util.OptionalBoolTrue}) - } - - if opts.Mirror != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"is_mirror": opts.Mirror == util.OptionalBoolTrue}) - } - - if opts.Actor != nil && opts.Actor.IsRestricted { - cond = cond.And(accessibleRepositoryCondition(opts.Actor)) - } - - if opts.Archived != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"is_archived": opts.Archived == util.OptionalBoolTrue}) - } - - switch opts.HasMilestones { - case util.OptionalBoolTrue: - cond = cond.And(builder.Gt{"num_milestones": 0}) - case util.OptionalBoolFalse: - cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"})) - } - - return cond -} - -// SearchRepository returns repositories based on search options, -// it returns results in given range and number of total results. -func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) { - cond := SearchRepositoryCondition(opts) - return SearchRepositoryByCondition(opts, cond, true) -} - -// SearchRepositoryByCondition search repositories by condition -func SearchRepositoryByCondition(opts *SearchRepoOptions, cond builder.Cond, loadAttributes bool) (RepositoryList, int64, error) { - ctx := db.DefaultContext - sess, count, err := searchRepositoryByCondition(ctx, opts, cond) - if err != nil { - return nil, 0, err - } - - defaultSize := 50 - if opts.PageSize > 0 { - defaultSize = opts.PageSize - } - repos := make(RepositoryList, 0, defaultSize) - if err := sess.Find(&repos); err != nil { - return nil, 0, fmt.Errorf("Repo: %v", err) - } - - if opts.PageSize <= 0 { - count = int64(len(repos)) - } - - if loadAttributes { - if err := repos.loadAttributes(ctx); err != nil { - return nil, 0, fmt.Errorf("LoadAttributes: %v", err) - } - } - - return repos, count, nil -} - -func searchRepositoryByCondition(ctx context.Context, opts *SearchRepoOptions, cond builder.Cond) (db.Engine, int64, error) { - if opts.Page <= 0 { - opts.Page = 1 - } - - if len(opts.OrderBy) == 0 { - opts.OrderBy = db.SearchOrderByAlphabetically - } - - args := make([]interface{}, 0) - if opts.PriorityOwnerID > 0 { - opts.OrderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_id = ? THEN 0 ELSE owner_id END, %s", opts.OrderBy)) - args = append(args, opts.PriorityOwnerID) - } else if strings.Count(opts.Keyword, "/") == 1 { - // With "owner/repo" search times, prioritise results which match the owner field - orgName := strings.Split(opts.Keyword, "/")[0] - opts.OrderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_name LIKE ? THEN 0 ELSE 1 END, %s", opts.OrderBy)) - args = append(args, orgName) - } - - sess := db.GetEngine(ctx) - - var count int64 - if opts.PageSize > 0 { - var err error - count, err = sess. - Where(cond). - Count(new(repo_model.Repository)) - if err != nil { - return nil, 0, fmt.Errorf("Count: %v", err) - } - } - - sess = sess.Where(cond).OrderBy(opts.OrderBy.String(), args...) - if opts.PageSize > 0 { - sess = sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) - } - return sess, count, nil -} - -// accessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible -func accessibleRepositoryCondition(user *user_model.User) builder.Cond { - cond := builder.NewCond() - - if user == nil || !user.IsRestricted || user.ID <= 0 { - orgVisibilityLimit := []structs.VisibleType{structs.VisibleTypePrivate} - if user == nil || user.ID <= 0 { - orgVisibilityLimit = append(orgVisibilityLimit, structs.VisibleTypeLimited) - } - // 1. Be able to see all non-private repositories that either: - cond = cond.Or(builder.And( - builder.Eq{"`repository`.is_private": false}, - // 2. Aren't in an private organisation or limited organisation if we're not logged in - builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where( - builder.And( - builder.Eq{"type": user_model.UserTypeOrganization}, - builder.In("visibility", orgVisibilityLimit)), - )))) - } - - if user != nil { - cond = cond.Or( - // 2. Be able to see all repositories that we have access to - userCollaborationRepoCond("`repository`.id", user.ID), - // 3. Repositories that we directly own - builder.Eq{"`repository`.owner_id": user.ID}, - // 4. Be able to see all repositories that we are in a team - userOrgTeamRepoCond("`repository`.id", user.ID), - // 5. Be able to see all public repos in private organizations that we are an org_user of - userOrgPublicRepoCond(user.ID), - ) - } - - return cond -} - -// SearchRepositoryByName takes keyword and part of repository name to search, -// it returns results in given range and number of total results. -func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, error) { - opts.IncludeDescription = false - return SearchRepository(opts) -} - -// SearchRepositoryIDs takes keyword and part of repository name to search, -// it returns results in given range and number of total results. -func SearchRepositoryIDs(opts *SearchRepoOptions) ([]int64, int64, error) { - opts.IncludeDescription = false - - cond := SearchRepositoryCondition(opts) - - sess, count, err := searchRepositoryByCondition(db.DefaultContext, opts, cond) - if err != nil { - return nil, 0, err - } - - defaultSize := 50 - if opts.PageSize > 0 { - defaultSize = opts.PageSize - } - - ids := make([]int64, 0, defaultSize) - err = sess.Select("id").Table("repository").Find(&ids) - if opts.PageSize <= 0 { - count = int64(len(ids)) - } - - return ids, count, err -} - -// AccessibleRepoIDsQuery queries accessible repository ids. Usable as a subquery wherever repo ids need to be filtered. -func AccessibleRepoIDsQuery(user *user_model.User) *builder.Builder { - // NB: Please note this code needs to still work if user is nil - return builder.Select("id").From("repository").Where(accessibleRepositoryCondition(user)) -} - -// FindUserAccessibleRepoIDs find all accessible repositories' ID by user's id -func FindUserAccessibleRepoIDs(user *user_model.User) ([]int64, error) { - repoIDs := make([]int64, 0, 10) - if err := db.GetEngine(db.DefaultContext). - Table("repository"). - Cols("id"). - Where(accessibleRepositoryCondition(user)). - Find(&repoIDs); err != nil { - return nil, fmt.Errorf("FindUserAccesibleRepoIDs: %v", err) - } - return repoIDs, nil -} - -// GetUserRepositories returns a list of repositories of given user. -func GetUserRepositories(opts *SearchRepoOptions) (RepositoryList, int64, error) { - if len(opts.OrderBy) == 0 { - opts.OrderBy = "updated_unix DESC" - } - - cond := builder.NewCond() - cond = cond.And(builder.Eq{"owner_id": opts.Actor.ID}) - if !opts.Private { - cond = cond.And(builder.Eq{"is_private": false}) - } - - if opts.LowerNames != nil && len(opts.LowerNames) > 0 { - cond = cond.And(builder.In("lower_name", opts.LowerNames)) - } - - sess := db.GetEngine(db.DefaultContext) - - count, err := sess.Where(cond).Count(new(repo_model.Repository)) - if err != nil { - return nil, 0, fmt.Errorf("Count: %v", err) - } - - sess = sess.Where(cond).OrderBy(opts.OrderBy.String()) - repos := make(RepositoryList, 0, opts.PageSize) - return repos, count, db.SetSessionPagination(sess, opts).Find(&repos) -} diff --git a/models/repo_test.go b/models/repo_test.go index dd1673f6b..c9e66398d 100644 --- a/models/repo_test.go +++ b/models/repo_test.go @@ -84,127 +84,8 @@ func TestMetas(t *testing.T) { assert.Equal(t, ",owners,team1,", metas["teams"]) } -func TestUpdateRepositoryVisibilityChanged(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // Get sample repo and change visibility - repo, err := repo_model.GetRepositoryByID(9) - assert.NoError(t, err) - repo.IsPrivate = true - - // Update it - err = UpdateRepository(repo, true) - assert.NoError(t, err) - - // Check visibility of action has become private - act := Action{} - _, err = db.GetEngine(db.DefaultContext).ID(3).Get(&act) - - assert.NoError(t, err) - assert.True(t, act.IsPrivate) -} - func TestDoctorUserStarNum(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, DoctorUserStarNum()) } - -func TestRepoGetReviewers(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - // test public repo - repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) - - reviewers, err := GetReviewers(repo1, 2, 2) - assert.NoError(t, err) - assert.Len(t, reviewers, 4) - - // should include doer if doer is not PR poster. - reviewers, err = GetReviewers(repo1, 11, 2) - assert.NoError(t, err) - assert.Len(t, reviewers, 4) - - // should not include PR poster, if PR poster would be otherwise eligible - reviewers, err = GetReviewers(repo1, 11, 4) - assert.NoError(t, err) - assert.Len(t, reviewers, 3) - - // test private user repo - repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) - - reviewers, err = GetReviewers(repo2, 2, 4) - assert.NoError(t, err) - assert.Len(t, reviewers, 1) - assert.EqualValues(t, reviewers[0].ID, 2) - - // test private org repo - repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository) - - reviewers, err = GetReviewers(repo3, 2, 1) - assert.NoError(t, err) - assert.Len(t, reviewers, 2) - - reviewers, err = GetReviewers(repo3, 2, 2) - assert.NoError(t, err) - assert.Len(t, reviewers, 1) -} - -func TestRepoGetReviewerTeams(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) - teams, err := GetReviewerTeams(repo2) - assert.NoError(t, err) - assert.Empty(t, teams) - - repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository) - teams, err = GetReviewerTeams(repo3) - assert.NoError(t, err) - assert.Len(t, teams, 2) -} - -func TestLinkedRepository(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - testCases := []struct { - name string - attachID int64 - expectedRepo *repo_model.Repository - expectedUnitType unit.Type - }{ - {"LinkedIssue", 1, &repo_model.Repository{ID: 1}, unit.TypeIssues}, - {"LinkedComment", 3, &repo_model.Repository{ID: 1}, unit.TypePullRequests}, - {"LinkedRelease", 9, &repo_model.Repository{ID: 1}, unit.TypeReleases}, - {"Notlinked", 10, nil, -1}, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - attach, err := repo_model.GetAttachmentByID(db.DefaultContext, tc.attachID) - assert.NoError(t, err) - repo, unitType, err := LinkedRepository(attach) - assert.NoError(t, err) - if tc.expectedRepo != nil { - assert.Equal(t, tc.expectedRepo.ID, repo.ID) - } - assert.Equal(t, tc.expectedUnitType, unitType) - }) - } -} - -func TestRepoAssignees(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) - users, err := GetRepoAssignees(repo2) - assert.NoError(t, err) - assert.Len(t, users, 1) - assert.Equal(t, users[0].ID, int64(2)) - - repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21}).(*repo_model.Repository) - users, err = GetRepoAssignees(repo21) - assert.NoError(t, err) - assert.Len(t, users, 3) - assert.Equal(t, users[0].ID, int64(15)) - assert.Equal(t, users[1].ID, int64(18)) - assert.Equal(t, users[2].ID, int64(16)) -} diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index 5eea97772..1b79a414a 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -397,6 +397,14 @@ func CreateWebhook(ctx context.Context, w *Webhook) error { return db.Insert(ctx, w) } +// CreateWebhooks creates multiple web hooks +func CreateWebhooks(ctx context.Context, ws []*Webhook) error { + for i := 0; i < len(ws); i++ { + ws[i].Type = strings.TrimSpace(ws[i].Type) + } + return db.Insert(ctx, ws) +} + // getWebhook uses argument bean as query condition, // ID must be specified and do not assign unnecessary fields. func getWebhook(bean *Webhook) (*Webhook, error) { diff --git a/modules/context/repo.go b/modules/context/repo.go index df3fe4e74..5f4af114f 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -26,6 +26,7 @@ import ( code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" + repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -551,14 +552,14 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { ctx.Data["CanWriteIssues"] = ctx.Repo.CanWrite(unit_model.TypeIssues) ctx.Data["CanWritePulls"] = ctx.Repo.CanWrite(unit_model.TypePullRequests) - canSignedUserFork, err := models.CanUserForkRepo(ctx.Doer, ctx.Repo.Repository) + canSignedUserFork, err := repo_module.CanUserForkRepo(ctx.Doer, ctx.Repo.Repository) if err != nil { ctx.ServerError("CanUserForkRepo", err) return } ctx.Data["CanSignedUserFork"] = canSignedUserFork - userAndOrgForks, err := models.GetForksByUserAndOrgs(ctx, ctx.Doer, ctx.Repo.Repository) + userAndOrgForks, err := repo_model.GetForksByUserAndOrgs(ctx, ctx.Doer, ctx.Repo.Repository) if err != nil { ctx.ServerError("GetForksByUserAndOrgs", err) return diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index 7adc938dc..85de4c75b 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -291,8 +291,8 @@ func populateIssueIndexer(ctx context.Context) { return default: } - repos, _, err := models.SearchRepositoryByName(&models.SearchRepoOptions{ - ListOptions: db.ListOptions{Page: page, PageSize: models.RepositoryListDefaultPageSize}, + repos, _, err := repo_model.SearchRepositoryByName(&repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{Page: page, PageSize: repo_model.RepositoryListDefaultPageSize}, OrderBy: db.SearchOrderByID, Private: true, Collaborate: util.OptionalBoolFalse, diff --git a/modules/repository/create.go b/modules/repository/create.go index 21d45c896..95bb82540 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -7,15 +7,20 @@ package repository import ( "context" "fmt" + "os" + "path" "strings" + "unicode/utf8" "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" ) @@ -108,7 +113,7 @@ func CreateRepository(doer, u *user_model.User, opts models.CreateRepoOptions) ( } } - if err := models.CheckDaemonExportOK(ctx, repo); err != nil { + if err := CheckDaemonExportOK(ctx, repo); err != nil { return fmt.Errorf("checkDaemonExportOK: %v", err) } @@ -133,3 +138,111 @@ func CreateRepository(doer, u *user_model.User, opts models.CreateRepoOptions) ( return repo, nil } + +// UpdateRepoSize updates the repository size, calculating it using util.GetDirectorySize +func UpdateRepoSize(ctx context.Context, repo *repo_model.Repository) error { + size, err := util.GetDirectorySize(repo.RepoPath()) + if err != nil { + return fmt.Errorf("updateSize: %v", err) + } + + lfsSize, err := models.GetRepoLFSSize(ctx, repo.ID) + if err != nil { + return fmt.Errorf("updateSize: GetLFSMetaObjects: %v", err) + } + + return repo_model.UpdateRepoSize(ctx, repo.ID, size+lfsSize) +} + +// CheckDaemonExportOK creates/removes git-daemon-export-ok for git-daemon... +func CheckDaemonExportOK(ctx context.Context, repo *repo_model.Repository) error { + if err := repo.GetOwner(ctx); err != nil { + return err + } + + // Create/Remove git-daemon-export-ok for git-daemon... + daemonExportFile := path.Join(repo.RepoPath(), `git-daemon-export-ok`) + + isExist, err := util.IsExist(daemonExportFile) + if err != nil { + log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err) + return err + } + + isPublic := !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePublic + if !isPublic && isExist { + if err = util.Remove(daemonExportFile); err != nil { + log.Error("Failed to remove %s: %v", daemonExportFile, err) + } + } else if isPublic && !isExist { + if f, err := os.Create(daemonExportFile); err != nil { + log.Error("Failed to create %s: %v", daemonExportFile, err) + } else { + f.Close() + } + } + + return nil +} + +// UpdateRepository updates a repository with db context +func UpdateRepository(ctx context.Context, repo *repo_model.Repository, visibilityChanged bool) (err error) { + repo.LowerName = strings.ToLower(repo.Name) + + if utf8.RuneCountInString(repo.Description) > 255 { + repo.Description = string([]rune(repo.Description)[:255]) + } + if utf8.RuneCountInString(repo.Website) > 255 { + repo.Website = string([]rune(repo.Website)[:255]) + } + + e := db.GetEngine(ctx) + + if _, err = e.ID(repo.ID).AllCols().Update(repo); err != nil { + return fmt.Errorf("update: %v", err) + } + + if err = UpdateRepoSize(ctx, repo); err != nil { + log.Error("Failed to update size for repository: %v", err) + } + + if visibilityChanged { + if err = repo.GetOwner(ctx); err != nil { + return fmt.Errorf("getOwner: %v", err) + } + if repo.Owner.IsOrganization() { + // Organization repository need to recalculate access table when visibility is changed. + if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil { + return fmt.Errorf("recalculateTeamAccesses: %v", err) + } + } + + // If repo has become private, we need to set its actions to private. + if repo.IsPrivate { + _, err = e.Where("repo_id = ?", repo.ID).Cols("is_private").Update(&models.Action{ + IsPrivate: true, + }) + if err != nil { + return err + } + } + + // Create/Remove git-daemon-export-ok for git-daemon... + if err := CheckDaemonExportOK(db.WithEngine(ctx, e), repo); err != nil { + return err + } + + forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID) + if err != nil { + return fmt.Errorf("getRepositoriesByForkID: %v", err) + } + for i := range forkRepos { + forkRepos[i].IsPrivate = repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate + if err = UpdateRepository(ctx, forkRepos[i], true); err != nil { + return fmt.Errorf("updateRepository[%d]: %v", forkRepos[i].ID, err) + } + } + } + + return nil +} diff --git a/modules/repository/create_test.go b/modules/repository/create_test.go index b6a89a7ed..2a47e9363 100644 --- a/modules/repository/create_test.go +++ b/modules/repository/create_test.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/structs" @@ -147,3 +148,23 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { } assert.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization") } + +func TestUpdateRepositoryVisibilityChanged(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // Get sample repo and change visibility + repo, err := repo_model.GetRepositoryByID(9) + assert.NoError(t, err) + repo.IsPrivate = true + + // Update it + err = UpdateRepository(db.DefaultContext, repo, true) + assert.NoError(t, err) + + // Check visibility of action has become private + act := models.Action{} + _, err = db.GetEngine(db.DefaultContext).ID(3).Get(&act) + + assert.NoError(t, err) + assert.True(t, act.IsPrivate) +} diff --git a/modules/repository/delete.go b/modules/repository/delete.go new file mode 100644 index 000000000..25fb15e30 --- /dev/null +++ b/modules/repository/delete.go @@ -0,0 +1,33 @@ +// Copyright 2022 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 repository + +import ( + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" +) + +// CanUserDelete returns true if user could delete the repository +func CanUserDelete(repo *repo_model.Repository, user *user_model.User) (bool, error) { + if user.IsAdmin || user.ID == repo.OwnerID { + return true, nil + } + + if err := repo.GetOwner(db.DefaultContext); err != nil { + return false, err + } + + if repo.Owner.IsOrganization() { + isOwner, err := organization.OrgFromUser(repo.Owner).IsOwnedBy(user.ID) + if err != nil { + return false, err + } + return isOwner, nil + } + + return false, nil +} diff --git a/modules/repository/fork.go b/modules/repository/fork.go new file mode 100644 index 000000000..c967d3b74 --- /dev/null +++ b/modules/repository/fork.go @@ -0,0 +1,31 @@ +// Copyright 2019 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 repository + +import ( + "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" +) + +// CanUserForkRepo returns true if specified user can fork repository. +func CanUserForkRepo(user *user_model.User, repo *repo_model.Repository) (bool, error) { + if user == nil { + return false, nil + } + if repo.OwnerID != user.ID && !repo_model.HasForkedRepo(user.ID, repo.ID) { + return true, nil + } + ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(user.ID) + if err != nil { + return false, err + } + for _, org := range ownedOrgs { + if repo.OwnerID != org.ID && !repo_model.HasForkedRepo(org.ID, repo.ID) { + return true, nil + } + } + return false, nil +} diff --git a/modules/repository/generate.go b/modules/repository/generate.go index b3ce80917..94bb6e642 100644 --- a/modules/repository/generate.go +++ b/modules/repository/generate.go @@ -5,6 +5,8 @@ package repository import ( + "bufio" + "bytes" "context" "fmt" "os" @@ -20,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" + "github.com/gobwas/glob" "github.com/huandu/xstrings" ) @@ -78,7 +81,38 @@ func generateExpansion(src string, templateRepo, generateRepo *repo_model.Reposi }) } -func checkGiteaTemplate(tmpDir string) (*models.GiteaTemplate, error) { +// GiteaTemplate holds information about a .gitea/template file +type GiteaTemplate struct { + Path string + Content []byte + + globs []glob.Glob +} + +// Globs parses the .gitea/template globs or returns them if they were already parsed +func (gt GiteaTemplate) Globs() []glob.Glob { + if gt.globs != nil { + return gt.globs + } + + gt.globs = make([]glob.Glob, 0) + scanner := bufio.NewScanner(bytes.NewReader(gt.Content)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + g, err := glob.Compile(line, '/') + if err != nil { + log.Info("Invalid glob expression '%s' (skipped): %v", line, err) + continue + } + gt.globs = append(gt.globs, g) + } + return gt.globs +} + +func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) { gtPath := filepath.Join(tmpDir, ".gitea", "template") if _, err := os.Stat(gtPath); os.IsNotExist(err) { return nil, nil @@ -91,7 +125,7 @@ func checkGiteaTemplate(tmpDir string) (*models.GiteaTemplate, error) { return nil, err } - gt := &models.GiteaTemplate{ + gt := &GiteaTemplate{ Path: gtPath, Content: content, } @@ -227,7 +261,7 @@ func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *r if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { return fmt.Errorf("setDefaultBranch: %v", err) } - if err = models.UpdateRepositoryCtx(ctx, repo, false); err != nil { + if err = UpdateRepository(ctx, repo, false); err != nil { return fmt.Errorf("updateRepository: %v", err) } @@ -240,7 +274,7 @@ func GenerateGitContent(ctx context.Context, templateRepo, generateRepo *repo_mo return err } - if err := models.UpdateRepoSize(ctx, generateRepo); err != nil { + if err := UpdateRepoSize(ctx, generateRepo); err != nil { return fmt.Errorf("failed to update size for repository: %v", err) } @@ -250,8 +284,27 @@ func GenerateGitContent(ctx context.Context, templateRepo, generateRepo *repo_mo return nil } +// GenerateRepoOptions contains the template units to generate +type GenerateRepoOptions struct { + Name string + DefaultBranch string + Description string + Private bool + GitContent bool + Topics bool + GitHooks bool + Webhooks bool + Avatar bool + IssueLabels bool +} + +// IsValid checks whether at least one option is chosen for generation +func (gro GenerateRepoOptions) IsValid() bool { + return gro.GitContent || gro.Topics || gro.GitHooks || gro.Webhooks || gro.Avatar || gro.IssueLabels // or other items as they are added +} + // GenerateRepository generates a repository from a template -func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts models.GenerateRepoOptions) (_ *repo_model.Repository, err error) { +func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts GenerateRepoOptions) (_ *repo_model.Repository, err error) { generateRepo := &repo_model.Repository{ OwnerID: owner.ID, Owner: owner, @@ -288,7 +341,7 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ return generateRepo, err } - if err = models.CheckDaemonExportOK(ctx, generateRepo); err != nil { + if err = CheckDaemonExportOK(ctx, generateRepo); err != nil { return generateRepo, fmt.Errorf("checkDaemonExportOK: %v", err) } diff --git a/models/repo_generate_test.go b/modules/repository/generate_test.go similarity index 98% rename from models/repo_generate_test.go rename to modules/repository/generate_test.go index e7a93433a..139fa4c91 100644 --- a/models/repo_generate_test.go +++ b/modules/repository/generate_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package models +package repository import ( "testing" diff --git a/modules/repository/init.go b/modules/repository/init.go index 845a61ed0..f8c7a8955 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -444,7 +444,7 @@ func initRepository(ctx context.Context, repoPath string, u *user_model.User, re } } - if err = models.UpdateRepositoryCtx(ctx, repo, false); err != nil { + if err = UpdateRepository(ctx, repo, false); err != nil { return fmt.Errorf("updateRepository: %v", err) } diff --git a/modules/repository/repo.go b/modules/repository/repo.go index 30ca6fdff..281999a1e 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -116,7 +116,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, repo.Owner = u } - if err = models.CheckDaemonExportOK(ctx, repo); err != nil { + if err = CheckDaemonExportOK(ctx, repo); err != nil { return repo, fmt.Errorf("checkDaemonExportOK: %v", err) } @@ -168,9 +168,11 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, } } - if err = models.UpdateRepoSize(ctx, repo); err != nil { - log.Error("Failed to update size for repository: %v", err) + ctx, committer, err := db.TxContext() + if err != nil { + return nil, err } + defer committer.Close() if opts.Mirror { mirrorModel := repo_model.Mirror{ @@ -203,17 +205,24 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, } } - if err = repo_model.InsertMirror(&mirrorModel); err != nil { + if err = repo_model.InsertMirror(ctx, &mirrorModel); err != nil { return repo, fmt.Errorf("InsertOne: %v", err) } repo.IsMirror = true - err = models.UpdateRepository(repo, false) + if err = UpdateRepository(ctx, repo, false); err != nil { + return nil, err + } } else { - repo, err = CleanUpMigrateInfo(ctx, repo) + if err = UpdateRepoSize(ctx, repo); err != nil { + log.Error("Failed to update size for repository: %v", err) + } + if repo, err = CleanUpMigrateInfo(ctx, repo); err != nil { + return nil, err + } } - return repo, err + return repo, committer.Commit() } // cleanUpMigrateGitConfig removes mirror info which prevents "push --all". @@ -253,7 +262,7 @@ func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo } } - return repo, models.UpdateRepository(repo, false) + return repo, UpdateRepository(ctx, repo, false) } // SyncReleasesWithTags synchronizes release table with repository tags diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index 248497a56..aa425e582 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -312,7 +312,7 @@ func GetReviewers(ctx *context.APIContext) { // "200": // "$ref": "#/responses/UserList" - reviewers, err := models.GetReviewers(ctx.Repo.Repository, ctx.Doer.ID, 0) + reviewers, err := repo_model.GetReviewers(ctx, ctx.Repo.Repository, ctx.Doer.ID, 0) if err != nil { ctx.Error(http.StatusInternalServerError, "ListCollaborators", err) return @@ -342,7 +342,7 @@ func GetAssignees(ctx *context.APIContext) { // "200": // "$ref": "#/responses/UserList" - assignees, err := models.GetRepoAssignees(ctx.Repo.Repository) + assignees, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "ListCollaborators", err) return diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 62959c3a7..c394ad175 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -17,6 +17,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" @@ -130,7 +131,7 @@ func SearchIssues(ctx *context.APIContext) { } // find repos user can access (for issue search) - opts := &models.SearchRepoOptions{ + opts := &repo_model.SearchRepoOptions{ Private: false, AllPublic: true, TopicOnly: false, @@ -176,8 +177,8 @@ func SearchIssues(ctx *context.APIContext) { opts.TeamID = team.ID } - repoCond := models.SearchRepositoryCondition(opts) - repoIDs, _, err := models.SearchRepositoryIDs(opts) + repoCond := repo_model.SearchRepositoryCondition(opts) + repoIDs, _, err := repo_model.SearchRepositoryIDs(opts) if err != nil { ctx.Error(http.StatusInternalServerError, "SearchRepositoryByName", err) return diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 8485ffbac..cdd1f7d5c 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -123,7 +123,7 @@ func Search(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - opts := &models.SearchRepoOptions{ + opts := &repo_model.SearchRepoOptions{ ListOptions: utils.GetListOptions(ctx), Actor: ctx.Doer, Keyword: ctx.FormTrim("q"), @@ -192,7 +192,7 @@ func Search(ctx *context.APIContext) { } var err error - repos, count, err := models.SearchRepository(opts) + repos, count, err := repo_model.SearchRepository(opts) if err != nil { ctx.JSON(http.StatusInternalServerError, api.SearchError{ OK: false, @@ -344,7 +344,7 @@ func Generate(ctx *context.APIContext) { return } - opts := models.GenerateRepoOptions{ + opts := repo_module.GenerateRepoOptions{ Name: form.Name, DefaultBranch: form.DefaultBranch, Description: form.Description, @@ -717,7 +717,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err repo.DefaultBranch = *opts.DefaultBranch } - if err := models.UpdateRepository(repo, visibilityChanged); err != nil { + if err := repo_service.UpdateRepository(repo, visibilityChanged); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateRepository", err) return err } @@ -1036,7 +1036,7 @@ func Delete(ctx *context.APIContext) { owner := ctx.Repo.Owner repo := ctx.Repo.Repository - canDelete, err := models.CanUserDelete(repo, ctx.Doer) + canDelete, err := repo_module.CanUserDelete(repo, ctx.Doer) if err != nil { ctx.Error(http.StatusInternalServerError, "CanUserDelete", err) return diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go index 05cecf508..709e3a6c5 100644 --- a/routers/api/v1/user/repo.go +++ b/routers/api/v1/user/repo.go @@ -7,9 +7,9 @@ package user import ( "net/http" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" @@ -21,7 +21,7 @@ import ( func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) { opts := utils.GetListOptions(ctx) - repos, count, err := models.GetUserRepositories(&models.SearchRepoOptions{ + repos, count, err := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{ Actor: u, Private: private, ListOptions: opts, @@ -103,7 +103,7 @@ func ListMyRepos(ctx *context.APIContext) { // "200": // "$ref": "#/responses/RepositoryList" - opts := &models.SearchRepoOptions{ + opts := &repo_model.SearchRepoOptions{ ListOptions: utils.GetListOptions(ctx), Actor: ctx.Doer, OwnerID: ctx.Doer.ID, @@ -112,7 +112,7 @@ func ListMyRepos(ctx *context.APIContext) { } var err error - repos, count, err := models.SearchRepository(opts) + repos, count, err := repo_model.SearchRepository(opts) if err != nil { ctx.Error(http.StatusInternalServerError, "SearchRepository", err) return diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go index 41ca27782..3fba2be37 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -55,7 +55,7 @@ func Code(ctx *context.Context) { // guest user or non-admin user if ctx.Doer == nil || !isAdmin { - repoIDs, err = models.FindUserAccessibleRepoIDs(ctx.Doer) + repoIDs, err = repo_model.FindUserAccessibleRepoIDs(ctx.Doer) if err != nil { ctx.ServerError("SearchResults", err) return @@ -79,7 +79,7 @@ func Code(ctx *context.Context) { rightRepoMap := make(map[int64]*repo_model.Repository, len(repoMaps)) repoIDs = make([]int64, 0, len(repoMaps)) for id, repo := range repoMaps { - if models.CheckRepoUnitUser(repo, ctx.Doer, unit.TypeCode) { + if models.CheckRepoUnitUser(ctx, repo, ctx.Doer, unit.TypeCode) { rightRepoMap[id] = repo repoIDs = append(repoIDs, id) } diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go index 3e8aa2bb0..f64642bc9 100644 --- a/routers/web/explore/repo.go +++ b/routers/web/explore/repo.go @@ -7,7 +7,6 @@ package explore import ( "net/http" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/base" @@ -81,7 +80,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { language := ctx.FormTrim("language") ctx.Data["Language"] = language - repos, count, err = models.SearchRepository(&models.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepository(&repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: page, PageSize: opts.PageSize, diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 24a0f13b5..d565a0c24 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -8,7 +8,6 @@ import ( "net/http" "strings" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" @@ -105,7 +104,7 @@ func Home(ctx *context.Context) { count int64 err error ) - repos, count, err = models.SearchRepository(&models.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepository(&repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: setting.UI.User.RepoPagingNum, Page: page, diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 758ca47af..c22a124e7 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -24,6 +24,7 @@ import ( user_setting "code.gitea.io/gitea/routers/web/user/setting" "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/org" + repo_service "code.gitea.io/gitea/services/repository" user_service "code.gitea.io/gitea/services/user" ) @@ -117,7 +118,7 @@ func SettingsPost(ctx *context.Context) { // update forks visibility if visibilityChanged { - repos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{ + repos, _, err := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{ Actor: org.AsUser(), Private: true, ListOptions: db.ListOptions{Page: 1, PageSize: org.NumRepos}, }) if err != nil { @@ -126,7 +127,7 @@ func SettingsPost(ctx *context.Context) { } for _, repo := range repos { repo.OwnerName = org.Name - if err := models.UpdateRepository(repo, true); err != nil { + if err := repo_service.UpdateRepository(repo, true); err != nil { ctx.ServerError("UpdateRepository", err) return } diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go index 701236f83..190dc6c2c 100644 --- a/routers/web/repo/attachment.go +++ b/routers/web/repo/attachment.go @@ -8,7 +8,6 @@ import ( "fmt" "net/http" - "code.gitea.io/gitea/models" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/context" @@ -19,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/upload" "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/services/attachment" + repo_service "code.gitea.io/gitea/services/repository" ) // UploadIssueAttachment response for Issue/PR attachments @@ -95,7 +95,7 @@ func GetAttachment(ctx *context.Context) { return } - repository, unitType, err := models.LinkedRepository(attach) + repository, unitType, err := repo_service.LinkedRepository(attach) if err != nil { ctx.ServerError("LinkedRepository", err) return diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 9f9448923..d3653f04e 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -457,7 +457,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { if rootRepo != nil && rootRepo.ID != ci.HeadRepo.ID && rootRepo.ID != baseRepo.ID { - canRead := models.CheckRepoUnitUser(rootRepo, ctx.Doer, unit.TypeCode) + canRead := models.CheckRepoUnitUser(ctx, rootRepo, ctx.Doer, unit.TypeCode) if canRead { ctx.Data["RootRepo"] = rootRepo if !fileOnly { @@ -482,7 +482,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { ownForkRepo.ID != ci.HeadRepo.ID && ownForkRepo.ID != baseRepo.ID && (rootRepo == nil || ownForkRepo.ID != rootRepo.ID) { - canRead := models.CheckRepoUnitUser(ownForkRepo, ctx.Doer, unit.TypeCode) + canRead := models.CheckRepoUnitUser(ctx, ownForkRepo, ctx.Doer, unit.TypeCode) if canRead { ctx.Data["OwnForkRepo"] = ownForkRepo if !fileOnly { diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index d418907a1..4a732ba45 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -49,6 +49,7 @@ import ( "code.gitea.io/gitea/services/forms" issue_service "code.gitea.io/gitea/services/issue" pull_service "code.gitea.io/gitea/services/pull" + repo_service "code.gitea.io/gitea/services/repository" ) const ( @@ -283,7 +284,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti ctx.Data["CommitStatuses"] = commitStatuses // Get assignees. - ctx.Data["Assignees"], err = models.GetRepoAssignees(repo) + ctx.Data["Assignees"], err = repo_model.GetRepoAssignees(ctx, repo) if err != nil { ctx.ServerError("GetAssignees", err) return @@ -441,7 +442,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R return } - ctx.Data["Assignees"], err = models.GetRepoAssignees(repo) + ctx.Data["Assignees"], err = repo_model.GetRepoAssignees(ctx, repo) if err != nil { ctx.ServerError("GetAssignees", err) return @@ -522,13 +523,13 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is posterID = 0 } - reviewers, err = models.GetReviewers(repo, ctx.Doer.ID, posterID) + reviewers, err = repo_model.GetReviewers(ctx, repo, ctx.Doer.ID, posterID) if err != nil { ctx.ServerError("GetReviewers", err) return } - teamReviewers, err = models.GetReviewerTeams(repo) + teamReviewers, err = repo_service.GetReviewerTeams(repo) if err != nil { ctx.ServerError("GetReviewerTeams", err) return @@ -2160,7 +2161,7 @@ func SearchIssues(ctx *context.Context) { } // find repos user can access (for issue search) - opts := &models.SearchRepoOptions{ + opts := &repo_model.SearchRepoOptions{ Private: false, AllPublic: true, TopicOnly: false, @@ -2206,8 +2207,8 @@ func SearchIssues(ctx *context.Context) { opts.TeamID = team.ID } - repoCond := models.SearchRepositoryCondition(opts) - repoIDs, _, err := models.SearchRepositoryIDs(opts) + repoCond := repo_model.SearchRepositoryCondition(opts) + repoIDs, _, err := repo_model.SearchRepositoryIDs(opts) if err != nil { ctx.Error(http.StatusInternalServerError, "SearchRepositoryByName", err.Error()) return diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 3f24be33d..8df4ccc60 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -758,7 +758,7 @@ func ViewPullFiles(ctx *context.Context) { setCompareContext(ctx, baseCommit, commit, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) ctx.Data["RequireTribute"] = true - if ctx.Data["Assignees"], err = models.GetRepoAssignees(ctx.Repo.Repository); err != nil { + if ctx.Data["Assignees"], err = repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository); err != nil { ctx.ServerError("GetAssignees", err) return } diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 30cb888dc..c2c79e4a0 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -152,7 +152,7 @@ func Create(ctx *context.Context) { templateID := ctx.FormInt64("template_id") if templateID > 0 { templateRepo, err := repo_model.GetRepositoryByID(templateID) - if err == nil && models.CheckRepoUnitUser(templateRepo, ctxUser, unit.TypeCode) { + if err == nil && models.CheckRepoUnitUser(ctx, templateRepo, ctxUser, unit.TypeCode) { ctx.Data["repo_template"] = templateID ctx.Data["repo_template_name"] = templateRepo.Name } @@ -223,7 +223,7 @@ func CreatePost(ctx *context.Context) { var repo *repo_model.Repository var err error if form.RepoTemplate > 0 { - opts := models.GenerateRepoOptions{ + opts := repo_module.GenerateRepoOptions{ Name: form.RepoName, Description: form.Description, Private: form.Private, @@ -304,7 +304,7 @@ func Action(ctx *context.Context) { ctx.Repo.Repository.Description = ctx.FormString("desc") ctx.Repo.Repository.Website = ctx.FormString("site") - err = models.UpdateRepository(ctx.Repo.Repository, false) + err = repo_service.UpdateRepository(ctx.Repo.Repository, false) } if err != nil { @@ -509,7 +509,7 @@ func InitiateDownload(ctx *context.Context) { // SearchRepo repositories via options func SearchRepo(ctx *context.Context) { - opts := &models.SearchRepoOptions{ + opts := &repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: ctx.FormInt("page"), PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), @@ -581,7 +581,7 @@ func SearchRepo(ctx *context.Context) { } var err error - repos, count, err := models.SearchRepository(opts) + repos, count, err := repo_model.SearchRepository(opts) if err != nil { ctx.JSON(http.StatusInternalServerError, api.SearchError{ OK: false, diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go index a60bf5262..b7be0aa3f 100644 --- a/routers/web/repo/setting.go +++ b/routers/web/repo/setting.go @@ -168,7 +168,7 @@ func SettingsPost(ctx *context.Context) { } repo.IsPrivate = form.Private - if err := models.UpdateRepository(repo, visibilityChanged); err != nil { + if err := repo_service.UpdateRepository(repo, visibilityChanged); err != nil { ctx.ServerError("UpdateRepository", err) return } @@ -491,7 +491,7 @@ func SettingsPost(ctx *context.Context) { return } if repoChanged { - if err := models.UpdateRepository(repo, false); err != nil { + if err := repo_service.UpdateRepository(repo, false); err != nil { ctx.ServerError("UpdateRepository", err) return } @@ -510,7 +510,7 @@ func SettingsPost(ctx *context.Context) { } if changed { - if err := models.UpdateRepository(repo, false); err != nil { + if err := repo_service.UpdateRepository(repo, false); err != nil { ctx.ServerError("UpdateRepository", err) return } @@ -530,7 +530,7 @@ func SettingsPost(ctx *context.Context) { repo.IsFsckEnabled = form.EnableHealthCheck } - if err := models.UpdateRepository(repo, false); err != nil { + if err := repo_service.UpdateRepository(repo, false); err != nil { ctx.ServerError("UpdateRepository", err) return } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 95ca81c44..2c9e16de5 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -34,6 +34,7 @@ import ( "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" + repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/typesniffer" @@ -905,7 +906,7 @@ func renderCode(ctx *context.Context) { ctx.ServerError("UpdateRepositoryCols", err) return } - if err = models.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { + if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { ctx.ServerError("UpdateRepoSize", err) return } diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 1412f6cfe..9b4fc652f 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -166,7 +166,7 @@ func Milestones(ctx *context.Context) { return } - repoOpts := models.SearchRepoOptions{ + repoOpts := repo_model.SearchRepoOptions{ Actor: ctxUser, OwnerID: ctxUser.ID, Private: true, @@ -181,7 +181,7 @@ func Milestones(ctx *context.Context) { } var ( - userRepoCond = models.SearchRepositoryCondition(&repoOpts) // all repo condition user could visit + userRepoCond = repo_model.SearchRepositoryCondition(&repoOpts) // all repo condition user could visit repoCond = userRepoCond repoIDs []int64 @@ -234,7 +234,7 @@ func Milestones(ctx *context.Context) { return } - showRepos, _, err := models.SearchRepositoryByCondition(&repoOpts, userRepoCond, false) + showRepos, _, err := repo_model.SearchRepositoryByCondition(&repoOpts, userRepoCond, false) if err != nil { ctx.ServerError("SearchRepositoryByCondition", err) return @@ -437,7 +437,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // As team: // - Team org's owns the repository. // - Team has read permission to repository. - repoOpts := &models.SearchRepoOptions{ + repoOpts := &repo_model.SearchRepoOptions{ Actor: ctx.Doer, OwnerID: ctx.Doer.ID, Private: true, @@ -559,7 +559,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { } // a RepositoryList - showRepos := models.RepositoryListOfMap(showReposMap) + showRepos := repo_model.RepositoryListOfMap(showReposMap) sort.Sort(showRepos) // maps pull request IDs to their CommitStatus. Will be posted to ctx.Data. diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go index bf78e00ad..9ad0711dc 100644 --- a/routers/web/user/home_test.go +++ b/routers/web/user/home_test.go @@ -8,7 +8,7 @@ import ( "net/http" "testing" - "code.gitea.io/gitea/models" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" @@ -26,7 +26,7 @@ func TestArchivedIssues(t *testing.T) { ctx.Req.Form.Set("state", "open") // Assume: User 30 has access to two Repos with Issues, one of the Repos being archived. - repos, _, _ := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctx.Doer}) + repos, _, _ := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{Actor: ctx.Doer}) assert.Len(t, repos, 2) IsArchived := make(map[int64]bool) NumIssues := make(map[int64]int) diff --git a/routers/web/user/package.go b/routers/web/user/package.go index c9aa2471e..b2b550cb7 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -7,7 +7,6 @@ package user import ( "net/http" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" container_model "code.gitea.io/gitea/models/packages/container" @@ -288,7 +287,7 @@ func PackageSettings(ctx *context.Context) { ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["PackageDescriptor"] = pd - repos, _, _ := models.GetUserRepositories(&models.SearchRepoOptions{ + repos, _, _ := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{ Actor: pd.Owner, Private: true, }) diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 8bce5460c..44501fc24 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -195,7 +195,7 @@ func Profile(ctx *context.Context) { } case "stars": ctx.Data["PageIsProfileStarList"] = true - repos, count, err = models.SearchRepository(&models.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepository(&repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: setting.UI.User.RepoPagingNum, Page: page, @@ -227,7 +227,7 @@ func Profile(ctx *context.Context) { return } case "watching": - repos, count, err = models.SearchRepository(&models.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepository(&repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: setting.UI.User.RepoPagingNum, Page: page, @@ -249,7 +249,7 @@ func Profile(ctx *context.Context) { total = int(count) default: - repos, count, err = models.SearchRepository(&models.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepository(&repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: setting.UI.User.RepoPagingNum, Page: page, diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index c2a406b18..972271269 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -15,7 +15,6 @@ import ( "path/filepath" "strings" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" @@ -304,7 +303,7 @@ func Repos(ctx *context.Context) { return } - userRepos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{ + userRepos, _, err := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{ Actor: ctxUser, Private: true, ListOptions: db.ListOptions{ @@ -329,7 +328,7 @@ func Repos(ctx *context.Context) { ctx.Data["Dirs"] = repoNames ctx.Data["ReposMap"] = repos } else { - repos, count64, err := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts}) + repos, count64, err := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts}) if err != nil { ctx.ServerError("GetUserRepositories", err) return diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 92ff92a6c..d611ff513 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -1220,7 +1220,7 @@ func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio oid := strings.TrimPrefix(line[1:], lfs.MetaFileOidPrefix) if len(oid) == 64 { m := &models.LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}} - count, err := db.Count(m) + count, err := db.CountByBean(db.DefaultContext, m) if err == nil && count > 0 { curFile.IsBin = true diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go index 4abf7eefd..d479dd0d4 100644 --- a/services/mailer/mail_issue.go +++ b/services/mailer/mail_issue.go @@ -145,7 +145,7 @@ func mailIssueCommentBatch(ctx *mailCommentContext, users []*user_model.User, vi visited[user.ID] = true // test if this user is allowed to see the issue/pull - if !models.CheckRepoUnitUser(ctx.Issue.Repo, user, checkUnit) { + if !models.CheckRepoUnitUser(ctx, ctx.Issue.Repo, user, checkUnit) { continue } diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index a93aee76c..caa81f0fe 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -10,7 +10,6 @@ import ( "strings" "time" - "code.gitea.io/gitea/models" admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" @@ -301,7 +300,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo gitRepo.Close() log.Trace("SyncMirrors [repo: %-v]: updating size of repository", m.Repo) - if err := models.UpdateRepoSize(ctx, m.Repo); err != nil { + if err := repo_module.UpdateRepoSize(ctx, m.Repo); err != nil { log.Error("SyncMirrors [repo: %-v]: failed to update size for mirror repository: %v", m.Repo, err) } diff --git a/services/repository/adopt.go b/services/repository/adopt.go index 1e8c22a47..48f049cd2 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -73,7 +73,7 @@ func AdoptRepository(doer, u *user_model.User, opts models.CreateRepoOptions) (* if err := adoptRepository(ctx, repoPath, doer, repo, opts); err != nil { return fmt.Errorf("createDelegateHooks: %v", err) } - if err := models.CheckDaemonExportOK(ctx, repo); err != nil { + if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil { return fmt.Errorf("checkDaemonExportOK: %v", err) } @@ -182,7 +182,7 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r } } - if err = models.UpdateRepositoryCtx(ctx, repo, false); err != nil { + if err = repo_module.UpdateRepository(ctx, repo, false); err != nil { return fmt.Errorf("updateRepository: %v", err) } @@ -246,7 +246,7 @@ func checkUnadoptedRepositories(userName string, repoNamesToCheck []string, unad } return err } - repos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{ + repos, _, err := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{ Actor: ctxUser, Private: true, ListOptions: db.ListOptions{ diff --git a/services/repository/check.go b/services/repository/check.go index 6fb86d0dc..17bdf2fac 100644 --- a/services/repository/check.go +++ b/services/repository/check.go @@ -17,6 +17,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" + repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/util" "xorm.io/builder" @@ -89,7 +90,7 @@ func GitGcRepos(ctx context.Context, timeout time.Duration, args ...string) erro } // Now update the size of the repository - if err := models.UpdateRepoSize(ctx, repo); err != nil { + if err := repo_module.UpdateRepoSize(ctx, repo); err != nil { log.Error("Updating size as part of garbage collection failed for %v. Stdout: %s\nError: %v", repo, stdout, err) desc := fmt.Sprintf("Updating size as part of garbage collection failed for %s. Stdout: %s\nError: %v", repo.RepoPath(), stdout, err) if err = admin_model.CreateRepositoryNotice(desc); err != nil { diff --git a/services/repository/fork.go b/services/repository/fork.go index a2ef75bbd..f4888cba1 100644 --- a/services/repository/fork.go +++ b/services/repository/fork.go @@ -96,7 +96,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork return err } - if err = models.IncrementRepoForkNum(txCtx, opts.BaseRepo.ID); err != nil { + if err = repo_model.IncrementRepoForkNum(txCtx, opts.BaseRepo.ID); err != nil { return err } @@ -116,7 +116,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork return fmt.Errorf("git clone: %v", err) } - if err := models.CheckDaemonExportOK(txCtx, repo); err != nil { + if err := repo_module.CheckDaemonExportOK(txCtx, repo); err != nil { return fmt.Errorf("checkDaemonExportOK: %v", err) } @@ -139,7 +139,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork } // even if below operations failed, it could be ignored. And they will be retried - if err := models.UpdateRepoSize(ctx, repo); err != nil { + if err := repo_module.UpdateRepoSize(ctx, repo); err != nil { log.Error("Failed to update size for repository: %v", err) } if err := repo_model.CopyLanguageStat(opts.BaseRepo, repo); err != nil { @@ -173,7 +173,7 @@ func ConvertForkToNormalRepository(repo *repo_model.Repository) error { return nil } - if err := models.DecrementRepoForkNum(ctx, repo.ForkID); err != nil { + if err := repo_model.DecrementRepoForkNum(ctx, repo.ForkID); err != nil { log.Error("Unable to decrement repo fork num for old root repo %d of repository %-v whilst converting from fork. Error: %v", repo.ForkID, repo, err) return err } @@ -181,7 +181,7 @@ func ConvertForkToNormalRepository(repo *repo_model.Repository) error { repo.IsFork = false repo.ForkID = 0 - if err := models.UpdateRepositoryCtx(ctx, repo, false); err != nil { + if err := repo_module.UpdateRepository(ctx, repo, false); err != nil { log.Error("Unable to update repository %-v whilst converting from fork. Error: %v", repo, err) return err } diff --git a/services/repository/hooks.go b/services/repository/hooks.go index 67931ffcb..45a031f38 100644 --- a/services/repository/hooks.go +++ b/services/repository/hooks.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" @@ -84,3 +85,29 @@ func GenerateGitHooks(ctx context.Context, templateRepo, generateRepo *repo_mode } return nil } + +// GenerateWebhooks generates webhooks from a template repository +func GenerateWebhooks(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) error { + templateWebhooks, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{RepoID: templateRepo.ID}) + if err != nil { + return err + } + + ws := make([]*webhook.Webhook, 0, len(templateWebhooks)) + for _, templateWebhook := range templateWebhooks { + ws = append(ws, &webhook.Webhook{ + RepoID: generateRepo.ID, + URL: templateWebhook.URL, + HTTPMethod: templateWebhook.HTTPMethod, + ContentType: templateWebhook.ContentType, + Secret: templateWebhook.Secret, + HookEvent: templateWebhook.HookEvent, + IsActive: templateWebhook.IsActive, + Type: templateWebhook.Type, + OrgID: templateWebhook.OrgID, + Events: templateWebhook.Events, + Meta: templateWebhook.Meta, + }) + } + return webhook.CreateWebhooks(ctx, ws) +} diff --git a/services/repository/push.go b/services/repository/push.go index 5e48a19ba..b6741c5ab 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -95,7 +95,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { } defer gitRepo.Close() - if err = models.UpdateRepoSize(ctx, repo); err != nil { + if err = repo_module.UpdateRepoSize(ctx, repo); err != nil { log.Error("Failed to update size for repository: %v", err) } diff --git a/services/repository/repository.go b/services/repository/repository.go index 6799ca586..6848eda10 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification" @@ -85,3 +86,42 @@ func Init() error { admin_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repositories", repo_module.LocalCopyPath()) return initPushQueue() } + +// UpdateRepository updates a repository +func UpdateRepository(repo *repo_model.Repository, visibilityChanged bool) (err error) { + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + if err = repo_module.UpdateRepository(ctx, repo, visibilityChanged); err != nil { + return fmt.Errorf("updateRepository: %v", err) + } + + return committer.Commit() +} + +// LinkedRepository returns the linked repo if any +func LinkedRepository(a *repo_model.Attachment) (*repo_model.Repository, unit.Type, error) { + if a.IssueID != 0 { + iss, err := models.GetIssueByID(a.IssueID) + if err != nil { + return nil, unit.TypeIssues, err + } + repo, err := repo_model.GetRepositoryByID(iss.RepoID) + unitType := unit.TypeIssues + if iss.IsPull { + unitType = unit.TypePullRequests + } + return repo, unitType, err + } else if a.ReleaseID != 0 { + rel, err := models.GetReleaseByID(db.DefaultContext, a.ReleaseID) + if err != nil { + return nil, unit.TypeReleases, err + } + repo, err := repo_model.GetRepositoryByID(rel.RepoID) + return repo, unit.TypeReleases, err + } + return nil, -1, nil +} diff --git a/services/repository/repository_test.go b/services/repository/repository_test.go new file mode 100644 index 000000000..e0ffcac3c --- /dev/null +++ b/services/repository/repository_test.go @@ -0,0 +1,43 @@ +// Copyright 2022 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 repository + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestLinkedRepository(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + testCases := []struct { + name string + attachID int64 + expectedRepo *repo_model.Repository + expectedUnitType unit.Type + }{ + {"LinkedIssue", 1, &repo_model.Repository{ID: 1}, unit.TypeIssues}, + {"LinkedComment", 3, &repo_model.Repository{ID: 1}, unit.TypePullRequests}, + {"LinkedRelease", 9, &repo_model.Repository{ID: 1}, unit.TypeReleases}, + {"Notlinked", 10, nil, -1}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + attach, err := repo_model.GetAttachmentByID(db.DefaultContext, tc.attachID) + assert.NoError(t, err) + repo, unitType, err := LinkedRepository(attach) + assert.NoError(t, err) + if tc.expectedRepo != nil { + assert.Equal(t, tc.expectedRepo.ID, repo.ID) + } + assert.Equal(t, tc.expectedUnitType, unitType) + }) + } +} diff --git a/services/repository/review.go b/services/repository/review.go new file mode 100644 index 000000000..9e8012978 --- /dev/null +++ b/services/repository/review.go @@ -0,0 +1,24 @@ +// Copyright 2022 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 repository + +import ( + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + repo_model "code.gitea.io/gitea/models/repo" +) + +// GetReviewerTeams get all teams can be requested to review +func GetReviewerTeams(repo *repo_model.Repository) ([]*organization.Team, error) { + if err := repo.GetOwner(db.DefaultContext); err != nil { + return nil, err + } + if !repo.Owner.IsOrganization() { + return nil, nil + } + + return organization.GetTeamsWithAccessToRepo(db.DefaultContext, repo.OwnerID, repo.ID, perm.AccessModeRead) +} diff --git a/services/repository/review_test.go b/services/repository/review_test.go new file mode 100644 index 000000000..640657d1d --- /dev/null +++ b/services/repository/review_test.go @@ -0,0 +1,28 @@ +// Copyright 2022 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 repository + +import ( + "testing" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestRepoGetReviewerTeams(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository) + teams, err := GetReviewerTeams(repo2) + assert.NoError(t, err) + assert.Empty(t, teams) + + repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository) + teams, err = GetReviewerTeams(repo3) + assert.NoError(t, err) + assert.Len(t, teams, 2) +} diff --git a/services/repository/template.go b/services/repository/template.go index 28fa1523a..6a1bfaff5 100644 --- a/services/repository/template.go +++ b/services/repository/template.go @@ -16,8 +16,27 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" ) +// GenerateIssueLabels generates issue labels from a template repository +func GenerateIssueLabels(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) error { + templateLabels, err := models.GetLabelsByRepoID(ctx, templateRepo.ID, "", db.ListOptions{}) + if err != nil { + return err + } + + newLabels := make([]*models.Label, 0, len(templateLabels)) + for _, templateLabel := range templateLabels { + newLabels = append(newLabels, &models.Label{ + RepoID: generateRepo.ID, + Name: templateLabel.Name, + Description: templateLabel.Description, + Color: templateLabel.Color, + }) + } + return db.Insert(ctx, newLabels) +} + // GenerateRepository generates a repository from a template -func GenerateRepository(doer, owner *user_model.User, templateRepo *repo_model.Repository, opts models.GenerateRepoOptions) (_ *repo_model.Repository, err error) { +func GenerateRepository(doer, owner *user_model.User, templateRepo *repo_model.Repository, opts repo_module.GenerateRepoOptions) (_ *repo_model.Repository, err error) { if !doer.IsAdmin && !owner.CanCreateRepo() { return nil, repo_model.ErrReachLimitOfRepo{ Limit: owner.MaxRepoCreation, @@ -54,7 +73,7 @@ func GenerateRepository(doer, owner *user_model.User, templateRepo *repo_model.R // Webhooks if opts.Webhooks { - if err = models.GenerateWebhooks(ctx, templateRepo, generateRepo); err != nil { + if err = GenerateWebhooks(ctx, templateRepo, generateRepo); err != nil { return err } } @@ -68,7 +87,7 @@ func GenerateRepository(doer, owner *user_model.User, templateRepo *repo_model.R // Issue Labels if opts.IssueLabels { - if err = models.GenerateIssueLabels(ctx, templateRepo, generateRepo); err != nil { + if err = GenerateIssueLabels(ctx, templateRepo, generateRepo); err != nil { return err } }