summaryrefslogtreecommitdiffstats
path: root/models/repo/user_repo.go
blob: 6790ee1da9379c22489aae4780a628eee03b8bfc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package repo

import (
	"context"

	"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"
	api "code.gitea.io/gitea/modules/structs"

	"xorm.io/builder"
)

// GetStarredRepos returns the repos starred by a particular user
func GetStarredRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, error) {
	sess := db.GetEngine(ctx).
		Where("star.uid=?", userID).
		Join("LEFT", "star", "`repository`.id=`star`.repo_id")
	if !private {
		sess = sess.And("is_private=?", false)
	}

	if listOptions.Page != 0 {
		sess = db.SetSessionPagination(sess, &listOptions)

		repos := make([]*Repository, 0, listOptions.PageSize)
		return repos, sess.Find(&repos)
	}

	repos := make([]*Repository, 0, 10)
	return repos, sess.Find(&repos)
}

// GetWatchedRepos returns the repos watched by a particular user
func GetWatchedRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, int64, error) {
	sess := db.GetEngine(ctx).
		Where("watch.user_id=?", userID).
		And("`watch`.mode<>?", WatchModeDont).
		Join("LEFT", "watch", "`repository`.id=`watch`.repo_id")
	if !private {
		sess = sess.And("is_private=?", false)
	}

	if listOptions.Page != 0 {
		sess = db.SetSessionPagination(sess, &listOptions)

		repos := make([]*Repository, 0, listOptions.PageSize)
		total, err := sess.FindAndCount(&repos)
		return repos, total, err
	}

	repos := make([]*Repository, 0, 10)
	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.LoadOwner(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 >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
			repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests).
		Distinct("`team_user`.uid").
		Select("`team_user`.uid").
		Find(&additionalUserIDs); err != nil {
		return nil, err
	}

	uniqueUserIDs := make(container.Set[int64])
	uniqueUserIDs.AddMultiple(userIDs...)
	uniqueUserIDs.AddMultiple(additionalUserIDs...)

	// 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(uniqueUserIDs)+1)
	if len(userIDs) > 0 {
		if err = e.In("id", uniqueUserIDs.Values()).
			Where(builder.Eq{"`user`.is_active": true}).
			OrderBy(user_model.GetOrderByName()).
			Find(&users); err != nil {
			return nil, err
		}
	}
	if !repo.Owner.IsOrganization() && !uniqueUserIDs.Contains(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.LoadOwner(ctx); err != nil {
		return nil, err
	}

	cond := builder.And(builder.Neq{"`user`.id": posterID}).
		And(builder.Eq{"`user`.is_active": true})

	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(user_model.GetOrderByName()).Find(&users)
}

// GetIssuePostersWithSearch returns users with limit of 30 whose username started with prefix that have authored an issue/pull request for the given repository
// If isShowFullName is set to true, also include full name prefix search
func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string, isShowFullName bool) ([]*user_model.User, error) {
	users := make([]*user_model.User, 0, 30)
	var prefixCond builder.Cond = builder.Like{"name", search + "%"}
	if isShowFullName {
		prefixCond = prefixCond.Or(builder.Like{"full_name", "%" + search + "%"})
	}

	cond := builder.In("`user`.id",
		builder.Select("poster_id").From("issue").Where(
			builder.Eq{"repo_id": repo.ID}.
				And(builder.Eq{"is_pull": isPull}),
		).GroupBy("poster_id")).And(prefixCond)

	return users, db.GetEngine(ctx).
		Where(cond).
		Cols("id", "name", "full_name", "avatar", "avatar_email", "use_custom_avatar").
		OrderBy("name").
		Limit(30).
		Find(&users)
}

// GetWatchedRepoIDsOwnedBy returns the repos owned by a particular user watched by a particular user
func GetWatchedRepoIDsOwnedBy(ctx context.Context, userID, ownedByUserID int64) ([]int64, error) {
	repoIDs := make([]int64, 0, 10)
	err := db.GetEngine(ctx).
		Table("repository").
		Select("`repository`.id").
		Join("LEFT", "watch", "`repository`.id=`watch`.repo_id").
		Where("`watch`.user_id=?", userID).
		And("`watch`.mode<>?", WatchModeDont).
		And("`repository`.owner_id=?", ownedByUserID).Find(&repoIDs)
	return repoIDs, err
}