forgejo/models/repo_list.go
zeripath 7a7f56044a
Adopt repositories (#12920)
* Don't automatically delete repository files if they are present

Prior to this PR Gitea would delete any repository files if they are
present during creation or migration. This can in certain circumstances
lead to data-loss and is slightly unpleasant.

This PR provides a mechanism for Gitea to adopt repositories on creation
and otherwise requires an explicit flag for deletion.

PushCreate is slightly different - the create will cause adoption if
that is allowed otherwise it will delete the data if that is allowed.

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Update swagger

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Fix tests and migrate overwrite

Signed-off-by: Andrew Thornton <art27@cantab.net>

* as per @lunny

Only offer to adopt or overwrite if the user can do that.

Allow the site administrator to adopt or overwrite in all
circumstances

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Use setting.Repository.DefaultBranch for the default branch

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Always set setting.Repository.DefaultBranch

Signed-off-by: Andrew Thornton <art27@cantab.net>

* update swagger

Signed-off-by: Andrew Thornton <art27@cantab.net>

* update templates

Signed-off-by: Andrew Thornton <art27@cantab.net>

* ensure repo closed

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Rewrite of adoption as per @6543 and @lunny

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Apply suggestions from code review

* update swagger

Signed-off-by: Andrew Thornton <art27@cantab.net>

* missing not

Signed-off-by: Andrew Thornton <art27@cantab.net>

* add modals and flash reporting

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Make the unadopted page searchable

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Add API

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Fix swagger

Signed-off-by: Andrew Thornton <art27@cantab.net>

* fix swagger

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Handle empty and non-master branched repositories

Signed-off-by: Andrew Thornton <art27@cantab.net>

* placate lint

Signed-off-by: Andrew Thornton <art27@cantab.net>

* remove commented out code

Signed-off-by: Andrew Thornton <art27@cantab.net>

Co-authored-by: techknowlogick <techknowlogick@gitea.io>
2020-09-25 07:09:23 +03:00

473 lines
15 KiB
Go

// 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 (
"fmt"
"strings"
"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 []*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]
}
// RepositoryListOfMap make list from values of map
func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList {
return RepositoryList(valuesRepository(repoMap))
}
func (repos RepositoryList) loadAttributes(e Engine) 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, len(set))
if err := e.
Where("id > 0").
In("id", 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 := e.
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(x)
}
// MirrorRepositoryList contains the mirror repositories
type MirrorRepositoryList []*Repository
func (repos MirrorRepositoryList) loadAttributes(e Engine) error {
if len(repos) == 0 {
return nil
}
// Load mirrors.
repoIDs := make([]int64, 0, len(repos))
for i := range repos {
if !repos[i].IsMirror {
continue
}
repoIDs = append(repoIDs, repos[i].ID)
}
mirrors := make([]*Mirror, 0, len(repoIDs))
if err := e.
Where("id > 0").
In("repo_id", repoIDs).
Find(&mirrors); err != nil {
return fmt.Errorf("find mirrors: %v", err)
}
set := make(map[int64]*Mirror)
for i := range mirrors {
set[mirrors[i].RepoID] = mirrors[i]
}
for i := range repos {
repos[i].Mirror = set[repos[i].ID]
}
return nil
}
// LoadAttributes loads the attributes for the given MirrorRepositoryList
func (repos MirrorRepositoryList) LoadAttributes() error {
return repos.loadAttributes(x)
}
// SearchRepoOptions holds the search options
type SearchRepoOptions struct {
ListOptions
Actor *User
Keyword string
OwnerID int64
PriorityOwnerID int64
OrderBy SearchOrderBy
Private bool // Include private repositories in results
StarredByID 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 -> incude just public
IsPrivate util.OptionalBool
// None -> include collaborative AND non-collaborative
// True -> include just collaborative
// False -> incude 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
// 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"
)
// SearchRepositoryCondition creates a query condition according search repository options
func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
var 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
// 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.And(
builder.Eq{"type": UserTypeOrganization},
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 repositories to those the OwnerID owns or contributes to as per opts.Collaborate
if opts.OwnerID > 0 {
var 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
builder.In("`repository`.id",
builder.Select("`access`.repo_id").
From("access").
Where(builder.Eq{"`access`.user_id": opts.OwnerID})),
// B. We are in a team for
builder.In("`repository`.id", builder.Select("`team_repo`.repo_id").
From("team_repo").
Where(builder.Eq{"`team_user`.uid": opts.OwnerID}).
Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id")),
// C. Public repositories in private organizations that we are member of
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": opts.OwnerID,
"`user`.type": UserTypeOrganization,
"`user`.visibility": structs.VisibleTypePrivate,
})))),
)
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.Keyword != "" {
// separate keyword
var 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")
var keywordCond = builder.In("id", subQuery)
if !opts.TopicOnly {
var likes = builder.NewCond()
for _, v := range strings.Split(opts.Keyword, ",") {
likes = likes.Or(builder.Like{"lower_name", strings.ToLower(v)})
if opts.IncludeDescription {
likes = likes.Or(builder.Like{"LOWER(description)", strings.ToLower(v)})
}
}
keywordCond = keywordCond.Or(likes)
}
cond = cond.And(keywordCond)
}
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) {
if opts.Page <= 0 {
opts.Page = 1
}
if len(opts.OrderBy) == 0 {
opts.OrderBy = SearchOrderByAlphabetically
}
if opts.PriorityOwnerID > 0 {
opts.OrderBy = SearchOrderBy(fmt.Sprintf("CASE WHEN owner_id = %d THEN 0 ELSE owner_id END, %s", opts.PriorityOwnerID, opts.OrderBy))
}
sess := x.NewSession()
defer sess.Close()
count, err := sess.
Where(cond).
Count(new(Repository))
if err != nil {
return nil, 0, fmt.Errorf("Count: %v", err)
}
repos := make(RepositoryList, 0, opts.PageSize)
sess.Where(cond).OrderBy(opts.OrderBy.String())
if opts.PageSize > 0 {
sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
}
if err = sess.Find(&repos); err != nil {
return nil, 0, fmt.Errorf("Repo: %v", err)
}
if loadAttributes {
if err = repos.loadAttributes(sess); err != nil {
return nil, 0, fmt.Errorf("LoadAttributes: %v", err)
}
}
return repos, count, nil
}
// accessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible
func accessibleRepositoryCondition(user *User) builder.Cond {
var 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": UserTypeOrganization},
builder.In("visibility", orgVisibilityLimit)),
))))
}
if user != nil {
cond = cond.Or(
// 2. Be able to see all repositories that we have access to
builder.In("`repository`.id", builder.Select("repo_id").
From("`access`").
Where(builder.And(
builder.Eq{"user_id": user.ID},
builder.Gt{"mode": int(AccessModeNone)}))),
// 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
builder.In("`repository`.id", builder.Select("`team_repo`.repo_id").
From("team_repo").
Where(builder.Eq{"`team_user`.uid": user.ID}).
Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id")),
// 5. Be able to see all public repos in private organizations that we are an org_user of
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": 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)
}
// AccessibleRepoIDsQuery queries accessible repository ids. Usable as a subquery wherever repo ids need to be filtered.
func AccessibleRepoIDsQuery(user *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) ([]int64, error) {
repoIDs := make([]int64, 0, 10)
if err := x.
Table("repository").
Cols("id").
Where(accessibleRepositoryCondition(user)).
Find(&repoIDs); err != nil {
return nil, fmt.Errorf("FindUserAccesibleRepoIDs: %v", err)
}
return repoIDs, nil
}