mirror of
https://github.com/go-gitea/gitea.git
synced 2024-09-01 14:56:30 +00:00
feat(pin): implemented base code for pins on the backend
- Created the pin model, and basic CRUD operations. - Implemented checks for lost of visibility of owner, to automatically delete the repository. - Implemented check for the lack of visibility of a viewer, when requesting the repos of another user, excluding those repositories from the list that is returned. - Added the deletion of all the pins of a repository when it is deleted. - Implemented the ability for a user to pin on the profile of an org user, checking if the user is admin of the org, and the org is owner of the repo. Co-authored-by: Daniel Carvalho <daniel.m.carvalho@tecnico.ulisboa.pt>
This commit is contained in:
parent
080486e47d
commit
5f890b55ca
65
models/repo/pin.go
Normal file
65
models/repo/pin.go
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
||||
type Pin struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UID int64 `xorm:"UNIQUE(s)"`
|
||||
RepoID int64 `xorm:"UNIQUE(s)"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
}
|
||||
|
||||
// TableName sets the table name for the pin struct
|
||||
func (s *Pin) TableName() string {
|
||||
return "repo_pin"
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Pin))
|
||||
}
|
||||
|
||||
func IsPinned(ctx context.Context, userID, repoID int64) bool {
|
||||
exists, _ := db.GetEngine(ctx).Get(&Pin{UID: userID, RepoID: repoID})
|
||||
return exists
|
||||
}
|
||||
|
||||
func PinRepo(ctx context.Context, doer *user_model.User, repo *Repository, pin bool) error {
|
||||
ctx, commiter, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer commiter.Close()
|
||||
pinned := IsPinned(ctx, doer.ID, repo.ID)
|
||||
|
||||
if pin {
|
||||
// Already pinned, nothing to do
|
||||
if pinned {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = db.Insert(ctx, &Pin{UID: doer.ID, RepoID: repo.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Not pinned, nothing to do
|
||||
if !pinned {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err = db.DeleteByBean(ctx, &Pin{UID: doer.ID, RepoID: repo.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return commiter.Commit()
|
||||
}
|
@ -54,6 +54,37 @@ func GetStarredRepos(ctx context.Context, opts *StarredReposOptions) ([]*Reposit
|
||||
return db.Find[Repository](ctx, opts)
|
||||
}
|
||||
|
||||
type PinnedReposOptions struct {
|
||||
db.ListOptions
|
||||
PinnerID int64
|
||||
RepoOwnerID int64
|
||||
}
|
||||
|
||||
func (opts *PinnedReposOptions) ToConds() builder.Cond {
|
||||
var cond builder.Cond = builder.Eq{
|
||||
"repo_pin.uid": opts.PinnerID,
|
||||
}
|
||||
if opts.RepoOwnerID != 0 {
|
||||
cond = cond.And(builder.Eq{
|
||||
"repository.owner_id": opts.RepoOwnerID,
|
||||
})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func (opts *PinnedReposOptions) ToJoins() []db.JoinFunc {
|
||||
return []db.JoinFunc{
|
||||
func(e db.Engine) error {
|
||||
e.Join("INNER", "repo_pin", "`repository`.id=`repo_pin`.repo_id")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func GetPinnedRepos(ctx context.Context, opts *PinnedReposOptions) (RepositoryList, error) {
|
||||
return db.Find[Repository](ctx, opts)
|
||||
}
|
||||
|
||||
type WatchedReposOptions struct {
|
||||
db.ListOptions
|
||||
WatcherID int64
|
||||
|
@ -151,6 +151,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
|
||||
&repo_model.Redirect{RedirectRepoID: repoID},
|
||||
&repo_model.RepoUnit{RepoID: repoID},
|
||||
&repo_model.Star{RepoID: repoID},
|
||||
&repo_model.Pin{RepoID: repoID},
|
||||
&admin_model.Task{RepoID: repoID},
|
||||
&repo_model.Watch{RepoID: repoID},
|
||||
&webhook.Webhook{RepoID: repoID},
|
||||
|
162
services/user/pin.go
Normal file
162
services/user/pin.go
Normal file
@ -0,0 +1,162 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
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/services/context"
|
||||
)
|
||||
|
||||
// Check if a user have a new pinned repo in it's profile, meaning that it
|
||||
// has permissions to pin said repo and also has enough space on the pinned list.
|
||||
func CanPin(ctx *context.Context, u *user_model.User, r *repo_model.Repository) bool {
|
||||
repos, err := repo_model.GetPinnedRepos(*ctx, &repo_model.PinnedReposOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
ListAll: true,
|
||||
},
|
||||
PinnerID: u.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetPinnedRepos", err)
|
||||
return false
|
||||
}
|
||||
if len(repos) >= 6 {
|
||||
return false
|
||||
}
|
||||
|
||||
return HasPermsToPin(ctx, u, r)
|
||||
}
|
||||
|
||||
// Checks if the user has permission to have the repo pinned in it's profile.
|
||||
func HasPermsToPin(ctx *context.Context, u *user_model.User, r *repo_model.Repository) bool {
|
||||
// If user is an organization, it can only pin its own repos
|
||||
if u.IsOrganization() {
|
||||
return r.OwnerID == u.ID
|
||||
}
|
||||
|
||||
// For normal users, anyone that has read access to the repo can pin it
|
||||
return canSeePin(ctx, u, r)
|
||||
}
|
||||
|
||||
// Check if a user can see a pin
|
||||
// A user can see a pin if he has read access to the repo
|
||||
func canSeePin(ctx *context.Context, u *user_model.User, r *repo_model.Repository) bool {
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, r, u)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
return false
|
||||
}
|
||||
return perm.HasAnyUnitAccess()
|
||||
}
|
||||
|
||||
// CleanupPins iterates over the repos pinned by a user and removes
|
||||
// the invalid pins. (Needs to be called everytime before we read/write a pin)
|
||||
func CleanupPins(ctx *context.Context, u *user_model.User) error {
|
||||
pinnedRepos, err := repo_model.GetPinnedRepos(*ctx, &repo_model.PinnedReposOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
ListAll: true,
|
||||
},
|
||||
PinnerID: u.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, repo := range pinnedRepos {
|
||||
if !HasPermsToPin(ctx, u, repo) {
|
||||
if err := repo_model.PinRepo(*ctx, u, repo, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns the pinned repos of a user that the viewer can see
|
||||
func GetUserPinnedRepos(ctx *context.Context, user, viewer *user_model.User) ([]*repo_model.Repository, error) {
|
||||
// Start by cleaning up the invalid pins
|
||||
err := CleanupPins(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get all of the user's pinned repos
|
||||
pinnedRepos, err := repo_model.GetPinnedRepos(*ctx, &repo_model.PinnedReposOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
ListAll: true,
|
||||
},
|
||||
PinnerID: user.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var repos []*repo_model.Repository
|
||||
|
||||
// Only include the repos that the viewer can see
|
||||
for _, repo := range pinnedRepos {
|
||||
if canSeePin(ctx, viewer, repo) {
|
||||
repos = append(repos, repo)
|
||||
}
|
||||
}
|
||||
|
||||
return repos, nil
|
||||
}
|
||||
|
||||
func PinRepo(ctx *context.Context, doer *user_model.User, repo *repo_model.Repository, pin, toOrg bool) error {
|
||||
// Determine the user which profile is the target for the pin
|
||||
var targetUser *user_model.User
|
||||
if toOrg {
|
||||
targetUser = repo.Owner
|
||||
} else {
|
||||
targetUser = doer
|
||||
}
|
||||
|
||||
// Start by cleaning up the invalid pins
|
||||
err := CleanupPins(ctx, targetUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If target is org profile, need to check if the doer can pin the repo
|
||||
// on said org profile
|
||||
if toOrg {
|
||||
err = assertUserOrgPerms(ctx, doer, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if pin {
|
||||
if !CanPin(ctx, targetUser, repo) {
|
||||
return errors.New("user cannot pin this repository")
|
||||
}
|
||||
}
|
||||
|
||||
return repo_model.PinRepo(*ctx, targetUser, repo, pin)
|
||||
}
|
||||
|
||||
func assertUserOrgPerms(ctx *context.Context, doer *user_model.User, repo *repo_model.Repository) error {
|
||||
if !ctx.Repo.Owner.IsOrganization() {
|
||||
return errors.New("owner is not an organization")
|
||||
}
|
||||
|
||||
isAdmin, err := organization.OrgFromUser(repo.Owner).IsOrgAdmin(ctx, doer.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isAdmin {
|
||||
return errors.New("user is not an admin of this organization")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user