mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
189 lines
5.8 KiB
Go
189 lines
5.8 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/git/models/db"
|
|
"github.com/hanzoai/git/models/organization"
|
|
"github.com/hanzoai/git/models/perm"
|
|
"github.com/hanzoai/git/models/unit"
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
"github.com/hanzoai/git/modules/container"
|
|
|
|
"github.com/hanzoai/builder"
|
|
)
|
|
|
|
type StarredReposOptions struct {
|
|
db.ListOptions
|
|
StarrerID int64
|
|
RepoOwnerID int64
|
|
IncludePrivate bool
|
|
// Actor is the user the private repositories are gated on: a private repo is only
|
|
// returned when Actor still has access to it, even if it was starred while access was granted.
|
|
Actor *user_model.User
|
|
}
|
|
|
|
func (opts *StarredReposOptions) ApplyPublicOnly(publicOnly bool) {
|
|
if publicOnly {
|
|
opts.IncludePrivate = false
|
|
}
|
|
}
|
|
|
|
func (opts *StarredReposOptions) ToConds() builder.Cond {
|
|
var cond builder.Cond = builder.Eq{
|
|
"star.uid": opts.StarrerID,
|
|
}
|
|
if opts.RepoOwnerID != 0 {
|
|
cond = cond.And(builder.Eq{
|
|
"repository.owner_id": opts.RepoOwnerID,
|
|
})
|
|
}
|
|
if opts.IncludePrivate {
|
|
// only include private repos the actor can still access, so metadata does not leak after access revocation
|
|
cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
|
|
} else {
|
|
// a public repo under a limited/private owner is not publicly reachable, so exclude it too
|
|
cond = cond.And(PublicRepoUnderPublicOwnerCond())
|
|
}
|
|
return cond
|
|
}
|
|
|
|
func (opts *StarredReposOptions) ToJoins() []db.JoinFunc {
|
|
return []db.JoinFunc{
|
|
func(e db.Engine) error {
|
|
e.Join("INNER", "star", "`repository`.id=`star`.repo_id")
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// GetStarredRepos returns the repos starred by a particular user
|
|
func GetStarredRepos(ctx context.Context, opts *StarredReposOptions) ([]*Repository, error) {
|
|
return db.Find[Repository](ctx, opts)
|
|
}
|
|
|
|
type WatchedReposOptions struct {
|
|
db.ListOptions
|
|
WatcherID int64
|
|
RepoOwnerID int64
|
|
IncludePrivate bool
|
|
// Actor is the user the private repositories are gated on: a private repo is only
|
|
// returned when Actor still has access to it, even if it was watched while access was granted.
|
|
Actor *user_model.User
|
|
}
|
|
|
|
func (opts *WatchedReposOptions) ApplyPublicOnly(publicOnly bool) {
|
|
if publicOnly {
|
|
opts.IncludePrivate = false
|
|
}
|
|
}
|
|
|
|
func (opts *WatchedReposOptions) ToConds() builder.Cond {
|
|
var cond builder.Cond = builder.Eq{
|
|
"watch.user_id": opts.WatcherID,
|
|
}
|
|
if opts.RepoOwnerID != 0 {
|
|
cond = cond.And(builder.Eq{
|
|
"repository.owner_id": opts.RepoOwnerID,
|
|
})
|
|
}
|
|
if opts.IncludePrivate {
|
|
// only include private repos the actor can still access, so metadata does not leak after access revocation
|
|
cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
|
|
} else {
|
|
// a public repo under a limited/private owner is not publicly reachable, so exclude it too
|
|
cond = cond.And(PublicRepoUnderPublicOwnerCond())
|
|
}
|
|
return cond.And(builder.Neq{
|
|
"watch.mode": WatchModeDont,
|
|
})
|
|
}
|
|
|
|
func (opts *WatchedReposOptions) ToJoins() []db.JoinFunc {
|
|
return []db.JoinFunc{
|
|
func(e db.Engine) error {
|
|
e.Join("INNER", "watch", "`repository`.id=`watch`.repo_id")
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// GetWatchedRepos returns the repos watched by a particular user
|
|
func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Repository, int64, error) {
|
|
return db.FindAndCount[Repository](ctx, opts)
|
|
}
|
|
|
|
// GetRepoAssignees returns all users that have write access and can be assigned to issues or pull-requests 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
|
|
}
|
|
|
|
uniqueUserIDs := make(container.Set[int64])
|
|
uniqueUserIDs.AddMultiple(userIDs...)
|
|
|
|
if repo.Owner.IsOrganization() {
|
|
// issues and pull requests both need "assignee list"
|
|
additionalUserIDs, err := organization.GetTeamUserIDsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypeIssues, unit.TypePullRequests)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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(uniqueUserIDs) > 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
|
|
}
|
|
|
|
// GetIssuePostersWithSearch returns users with limit of 30 whose username started with prefix that have authored an issue/pull request for the given repository
|
|
// It searches with the "user.name" and "user.full_name" fields case-insensitively.
|
|
func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string) ([]*user_model.User, error) {
|
|
users := make([]*user_model.User, 0, 30)
|
|
|
|
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"))
|
|
|
|
if search != "" {
|
|
var prefixCond builder.Cond = builder.Like{"lower_name", strings.ToLower(search) + "%"}
|
|
prefixCond = prefixCond.Or(db.BuildCaseInsensitiveLike("full_name", "%"+search+"%"))
|
|
cond = cond.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)
|
|
}
|