gogs/internal/database/repositories.go

406 lines
11 KiB
Go

// Copyright 2020 The Gogs 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 database
import (
"context"
"fmt"
"strings"
"time"
api "github.com/gogs/go-gogs-client"
"github.com/pkg/errors"
"gorm.io/gorm"
"gogs.io/gogs/internal/errutil"
"gogs.io/gogs/internal/repoutil"
)
// BeforeCreate implements the GORM create hook.
func (r *Repository) BeforeCreate(tx *gorm.DB) error {
if r.CreatedUnix == 0 {
r.CreatedUnix = tx.NowFunc().Unix()
}
return nil
}
// BeforeUpdate implements the GORM update hook.
func (r *Repository) BeforeUpdate(tx *gorm.DB) error {
r.UpdatedUnix = tx.NowFunc().Unix()
return nil
}
// AfterFind implements the GORM query hook.
func (r *Repository) AfterFind(_ *gorm.DB) error {
r.Created = time.Unix(r.CreatedUnix, 0).Local()
r.Updated = time.Unix(r.UpdatedUnix, 0).Local()
return nil
}
type RepositoryAPIFormatOptions struct {
Permission *api.Permission
Parent *api.Repository
}
// APIFormat returns the API format of a repository.
func (r *Repository) APIFormat(owner *User, opts ...RepositoryAPIFormatOptions) *api.Repository {
var opt RepositoryAPIFormatOptions
if len(opts) > 0 {
opt = opts[0]
}
cloneLink := repoutil.NewCloneLink(owner.Name, r.Name, false)
return &api.Repository{
ID: r.ID,
Owner: owner.APIFormat(),
Name: r.Name,
FullName: owner.Name + "/" + r.Name,
Description: r.Description,
Private: r.IsPrivate,
Fork: r.IsFork,
Parent: opt.Parent,
Empty: r.IsBare,
Mirror: r.IsMirror,
Size: r.Size,
HTMLURL: repoutil.HTMLURL(owner.Name, r.Name),
SSHURL: cloneLink.SSH,
CloneURL: cloneLink.HTTPS,
Website: r.Website,
Stars: r.NumStars,
Forks: r.NumForks,
Watchers: r.NumWatches,
OpenIssues: r.NumOpenIssues,
DefaultBranch: r.DefaultBranch,
Created: r.Created,
Updated: r.Updated,
Permissions: opt.Permission,
}
}
// RepositoriesStore is the storage layer for repositories.
type RepositoriesStore struct {
db *gorm.DB
}
func newReposStore(db *gorm.DB) *RepositoriesStore {
return &RepositoriesStore{db: db}
}
type ErrRepoAlreadyExist struct {
args errutil.Args
}
func IsErrRepoAlreadyExist(err error) bool {
_, ok := err.(ErrRepoAlreadyExist)
return ok
}
func (err ErrRepoAlreadyExist) Error() string {
return fmt.Sprintf("repository already exists: %v", err.args)
}
type CreateRepoOptions struct {
Name string
Description string
DefaultBranch string
Private bool
Mirror bool
EnableWiki bool
EnableIssues bool
EnablePulls bool
Fork bool
ForkID int64
}
// Create creates a new repository record in the database. It returns
// ErrNameNotAllowed when the repository name is not allowed, or
// ErrRepoAlreadyExist when a repository with same name already exists for the
// owner.
func (s *RepositoriesStore) Create(ctx context.Context, ownerID int64, opts CreateRepoOptions) (*Repository, error) {
err := isRepoNameAllowed(opts.Name)
if err != nil {
return nil, err
}
_, err = s.GetByName(ctx, ownerID, opts.Name)
if err == nil {
return nil, ErrRepoAlreadyExist{
args: errutil.Args{
"ownerID": ownerID,
"name": opts.Name,
},
}
} else if !IsErrRepoNotExist(err) {
return nil, err
}
repo := &Repository{
OwnerID: ownerID,
LowerName: strings.ToLower(opts.Name),
Name: opts.Name,
Description: opts.Description,
DefaultBranch: opts.DefaultBranch,
IsPrivate: opts.Private,
IsMirror: opts.Mirror,
EnableWiki: opts.EnableWiki,
EnableIssues: opts.EnableIssues,
EnablePulls: opts.EnablePulls,
IsFork: opts.Fork,
ForkID: opts.ForkID,
}
return repo, s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
err = tx.Create(repo).Error
if err != nil {
return errors.Wrap(err, "create")
}
err = newReposStore(tx).Watch(ctx, ownerID, repo.ID)
if err != nil {
return errors.Wrap(err, "watch")
}
return nil
})
}
// GetByCollaboratorID returns a list of repositories that the given
// collaborator has access to. Results are limited to the given limit and sorted
// by the given order (e.g. "updated_unix DESC"). Repositories that are owned
// directly by the given collaborator are not included.
func (s *RepositoriesStore) GetByCollaboratorID(ctx context.Context, collaboratorID int64, limit int, orderBy string) ([]*Repository, error) {
/*
Equivalent SQL for PostgreSQL:
SELECT * FROM repository
JOIN access ON access.repo_id = repository.id AND access.user_id = @collaboratorID
WHERE access.mode >= @accessModeRead
ORDER BY @orderBy
LIMIT @limit
*/
var repos []*Repository
return repos, s.db.WithContext(ctx).
Joins("JOIN access ON access.repo_id = repository.id AND access.user_id = ?", collaboratorID).
Where("access.mode >= ?", AccessModeRead).
Order(orderBy).
Limit(limit).
Find(&repos).
Error
}
// GetByCollaboratorIDWithAccessMode returns a list of repositories and
// corresponding access mode that the given collaborator has access to.
// Repositories that are owned directly by the given collaborator are not
// included.
func (s *RepositoriesStore) GetByCollaboratorIDWithAccessMode(ctx context.Context, collaboratorID int64) (map[*Repository]AccessMode, error) {
/*
Equivalent SQL for PostgreSQL:
SELECT
repository.*,
access.mode
FROM repository
JOIN access ON access.repo_id = repository.id AND access.user_id = @collaboratorID
WHERE access.mode >= @accessModeRead
*/
var reposWithAccessMode []*struct {
*Repository
Mode AccessMode
}
err := s.db.WithContext(ctx).
Select("repository.*", "access.mode").
Table("repository").
Joins("JOIN access ON access.repo_id = repository.id AND access.user_id = ?", collaboratorID).
Where("access.mode >= ?", AccessModeRead).
Find(&reposWithAccessMode).
Error
if err != nil {
return nil, err
}
repos := make(map[*Repository]AccessMode, len(reposWithAccessMode))
for _, repoWithAccessMode := range reposWithAccessMode {
repos[repoWithAccessMode.Repository] = repoWithAccessMode.Mode
}
return repos, nil
}
var _ errutil.NotFound = (*ErrRepoNotExist)(nil)
type ErrRepoNotExist struct {
args errutil.Args
}
func IsErrRepoNotExist(err error) bool {
_, ok := err.(ErrRepoNotExist)
return ok
}
func (err ErrRepoNotExist) Error() string {
return fmt.Sprintf("repository does not exist: %v", err.args)
}
func (ErrRepoNotExist) NotFound() bool {
return true
}
// GetByID returns the repository with given ID. It returns ErrRepoNotExist when
// not found.
func (s *RepositoriesStore) GetByID(ctx context.Context, id int64) (*Repository, error) {
repo := new(Repository)
err := s.db.WithContext(ctx).Where("id = ?", id).First(repo).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrRepoNotExist{errutil.Args{"repoID": id}}
}
return nil, err
}
return repo, nil
}
// GetByName returns the repository with given owner and name. It returns
// ErrRepoNotExist when not found.
func (s *RepositoriesStore) GetByName(ctx context.Context, ownerID int64, name string) (*Repository, error) {
repo := new(Repository)
err := s.db.WithContext(ctx).
Where("owner_id = ? AND lower_name = ?", ownerID, strings.ToLower(name)).
First(repo).
Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, ErrRepoNotExist{
args: errutil.Args{
"ownerID": ownerID,
"name": name,
},
}
}
return nil, err
}
return repo, nil
}
func (s *RepositoriesStore) recountStars(tx *gorm.DB, userID, repoID int64) error {
/*
Equivalent SQL for PostgreSQL:
UPDATE repository
SET num_stars = (
SELECT COUNT(*) FROM star WHERE repo_id = @repoID
)
WHERE id = @repoID
*/
err := tx.Model(&Repository{}).
Where("id = ?", repoID).
Update(
"num_stars",
tx.Model(&Star{}).Select("COUNT(*)").Where("repo_id = ?", repoID),
).
Error
if err != nil {
return errors.Wrap(err, `update "repository.num_stars"`)
}
/*
Equivalent SQL for PostgreSQL:
UPDATE "user"
SET num_stars = (
SELECT COUNT(*) FROM star WHERE uid = @userID
)
WHERE id = @userID
*/
err = tx.Model(&User{}).
Where("id = ?", userID).
Update(
"num_stars",
tx.Model(&Star{}).Select("COUNT(*)").Where("uid = ?", userID),
).
Error
if err != nil {
return errors.Wrap(err, `update "user.num_stars"`)
}
return nil
}
// Star marks the user to star the repository.
func (s *RepositoriesStore) Star(ctx context.Context, userID, repoID int64) error {
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
star := &Star{
UserID: userID,
RepoID: repoID,
}
result := tx.FirstOrCreate(star, star)
if result.Error != nil {
return errors.Wrap(result.Error, "upsert")
} else if result.RowsAffected <= 0 {
return nil // Relation already exists
}
return s.recountStars(tx, userID, repoID)
})
}
// Touch updates the updated time to the current time and removes the bare state
// of the given repository.
func (s *RepositoriesStore) Touch(ctx context.Context, id int64) error {
return s.db.WithContext(ctx).
Model(new(Repository)).
Where("id = ?", id).
Updates(map[string]any{
"is_bare": false,
"updated_unix": s.db.NowFunc().Unix(),
}).
Error
}
// ListWatches returns all watches of the given repository.
func (s *RepositoriesStore) ListWatches(ctx context.Context, repoID int64) ([]*Watch, error) {
var watches []*Watch
return watches, s.db.WithContext(ctx).Where("repo_id = ?", repoID).Find(&watches).Error
}
func (s *RepositoriesStore) recountWatches(tx *gorm.DB, repoID int64) error {
/*
Equivalent SQL for PostgreSQL:
UPDATE repository
SET num_watches = (
SELECT COUNT(*) FROM watch WHERE repo_id = @repoID
)
WHERE id = @repoID
*/
return tx.Model(&Repository{}).
Where("id = ?", repoID).
Update(
"num_watches",
tx.Model(&Watch{}).Select("COUNT(*)").Where("repo_id = ?", repoID),
).
Error
}
// Watch marks the user to watch the repository.
func (s *RepositoriesStore) Watch(ctx context.Context, userID, repoID int64) error {
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
w := &Watch{
UserID: userID,
RepoID: repoID,
}
result := tx.FirstOrCreate(w, w)
if result.Error != nil {
return errors.Wrap(result.Error, "upsert")
} else if result.RowsAffected <= 0 {
return nil // Relation already exists
}
return s.recountWatches(tx, repoID)
})
}
// HasForkedBy returns true if the given repository has forked by the given user.
func (s *RepositoriesStore) HasForkedBy(ctx context.Context, repoID, userID int64) bool {
var count int64
s.db.WithContext(ctx).Model(new(Repository)).Where("owner_id = ? AND fork_id = ?", userID, repoID).Count(&count)
return count > 0
}