2020-08-16 22:07:38 -05:00
// Copyright 2020 The Gitea Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2020-08-16 22:07:38 -05:00
2022-03-29 09:16:31 -05:00
package project
2020-08-16 22:07:38 -05:00
import (
2022-03-29 09:16:31 -05:00
"context"
2020-08-16 22:07:38 -05:00
"fmt"
2021-09-19 06:49:59 -05:00
"code.gitea.io/gitea/models/db"
2023-01-20 06:42:33 -05:00
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
2020-08-16 22:07:38 -05:00
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
type (
// ProjectsConfig is used to identify the type of board that is being created
ProjectsConfig struct {
2022-03-29 09:16:31 -05:00
BoardType BoardType
2020-08-16 22:07:38 -05:00
Translation string
}
2022-03-29 09:16:31 -05:00
// Type is used to identify the type of project in question and ownership
Type uint8
2020-08-16 22:07:38 -05:00
)
const (
2022-03-29 09:16:31 -05:00
// TypeIndividual is a type of project board that is owned by an individual
TypeIndividual Type = iota + 1
2020-08-16 22:07:38 -05:00
2022-03-29 09:16:31 -05:00
// TypeRepository is a project that is tied to a repository
TypeRepository
2020-08-16 22:07:38 -05:00
2022-03-29 09:16:31 -05:00
// TypeOrganization is a project that is tied to an organisation
TypeOrganization
2020-08-16 22:07:38 -05:00
)
2022-03-29 09:16:31 -05:00
// ErrProjectNotExist represents a "ProjectNotExist" kind of error.
type ErrProjectNotExist struct {
ID int64
RepoID int64
}
// IsErrProjectNotExist checks if an error is a ErrProjectNotExist
func IsErrProjectNotExist ( err error ) bool {
_ , ok := err . ( ErrProjectNotExist )
return ok
}
func ( err ErrProjectNotExist ) Error ( ) string {
return fmt . Sprintf ( "projects does not exist [id: %d]" , err . ID )
}
2022-10-18 00:50:37 -05:00
func ( err ErrProjectNotExist ) Unwrap ( ) error {
return util . ErrNotExist
}
2022-03-29 09:16:31 -05:00
// ErrProjectBoardNotExist represents a "ProjectBoardNotExist" kind of error.
type ErrProjectBoardNotExist struct {
BoardID int64
}
// IsErrProjectBoardNotExist checks if an error is a ErrProjectBoardNotExist
func IsErrProjectBoardNotExist ( err error ) bool {
_ , ok := err . ( ErrProjectBoardNotExist )
return ok
}
func ( err ErrProjectBoardNotExist ) Error ( ) string {
return fmt . Sprintf ( "project board does not exist [id: %d]" , err . BoardID )
}
2022-10-18 00:50:37 -05:00
func ( err ErrProjectBoardNotExist ) Unwrap ( ) error {
return util . ErrNotExist
}
2020-08-16 22:07:38 -05:00
// Project represents a project board
type Project struct {
2023-01-20 06:42:33 -05:00
ID int64 ` xorm:"pk autoincr" `
Title string ` xorm:"INDEX NOT NULL" `
Description string ` xorm:"TEXT" `
OwnerID int64 ` xorm:"INDEX" `
Owner * user_model . User ` xorm:"-" `
RepoID int64 ` xorm:"INDEX" `
Repo * repo_model . Repository ` xorm:"-" `
CreatorID int64 ` xorm:"NOT NULL" `
IsClosed bool ` xorm:"INDEX" `
2022-03-29 09:16:31 -05:00
BoardType BoardType
Type Type
2020-08-16 22:07:38 -05:00
RenderedContent string ` xorm:"-" `
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX created" `
UpdatedUnix timeutil . TimeStamp ` xorm:"INDEX updated" `
ClosedDateUnix timeutil . TimeStamp
}
2023-01-20 06:42:33 -05:00
func ( p * Project ) LoadOwner ( ctx context . Context ) ( err error ) {
if p . Owner != nil {
return nil
}
p . Owner , err = user_model . GetUserByID ( ctx , p . OwnerID )
return err
}
func ( p * Project ) LoadRepo ( ctx context . Context ) ( err error ) {
if p . RepoID == 0 || p . Repo != nil {
return nil
}
p . Repo , err = repo_model . GetRepositoryByID ( ctx , p . RepoID )
return err
}
func ( p * Project ) Link ( ) string {
if p . OwnerID > 0 {
err := p . LoadOwner ( db . DefaultContext )
if err != nil {
log . Error ( "LoadOwner: %v" , err )
return ""
}
2023-01-23 16:51:18 -05:00
return fmt . Sprintf ( "%s/-/projects/%d" , p . Owner . HomeLink ( ) , p . ID )
2023-01-20 06:42:33 -05:00
}
if p . RepoID > 0 {
err := p . LoadRepo ( db . DefaultContext )
if err != nil {
log . Error ( "LoadRepo: %v" , err )
return ""
}
2023-01-23 16:51:18 -05:00
return fmt . Sprintf ( "%s/projects/%d" , p . Repo . Link ( ) , p . ID )
2023-01-20 06:42:33 -05:00
}
return ""
}
func ( p * Project ) IsOrganizationProject ( ) bool {
return p . Type == TypeOrganization
}
2021-09-19 06:49:59 -05:00
func init ( ) {
db . RegisterModel ( new ( Project ) )
}
2020-08-16 22:07:38 -05:00
// GetProjectsConfig retrieves the types of configurations projects could have
func GetProjectsConfig ( ) [ ] ProjectsConfig {
return [ ] ProjectsConfig {
2022-03-29 09:16:31 -05:00
{ BoardTypeNone , "repo.projects.type.none" } ,
{ BoardTypeBasicKanban , "repo.projects.type.basic_kanban" } ,
{ BoardTypeBugTriage , "repo.projects.type.bug_triage" } ,
2020-08-16 22:07:38 -05:00
}
}
2022-03-29 09:16:31 -05:00
// IsTypeValid checks if a project type is valid
func IsTypeValid ( p Type ) bool {
2020-08-16 22:07:38 -05:00
switch p {
2023-01-20 06:42:33 -05:00
case TypeRepository , TypeOrganization :
2020-08-16 22:07:38 -05:00
return true
default :
return false
}
}
2022-03-29 09:16:31 -05:00
// SearchOptions are options for GetProjects
type SearchOptions struct {
2023-01-20 06:42:33 -05:00
OwnerID int64
2020-08-16 22:07:38 -05:00
RepoID int64
Page int
IsClosed util . OptionalBool
SortType string
2022-03-29 09:16:31 -05:00
Type Type
2020-08-16 22:07:38 -05:00
}
2023-01-20 06:42:33 -05:00
func ( opts * SearchOptions ) toConds ( ) builder . Cond {
cond := builder . NewCond ( )
if opts . RepoID > 0 {
cond = cond . And ( builder . Eq { "repo_id" : opts . RepoID } )
}
2020-08-16 22:07:38 -05:00
switch opts . IsClosed {
case util . OptionalBoolTrue :
cond = cond . And ( builder . Eq { "is_closed" : true } )
case util . OptionalBoolFalse :
cond = cond . And ( builder . Eq { "is_closed" : false } )
}
if opts . Type > 0 {
cond = cond . And ( builder . Eq { "type" : opts . Type } )
}
2023-01-20 06:42:33 -05:00
if opts . OwnerID > 0 {
cond = cond . And ( builder . Eq { "owner_id" : opts . OwnerID } )
}
return cond
}
// CountProjects counts projects
func CountProjects ( ctx context . Context , opts SearchOptions ) ( int64 , error ) {
return db . GetEngine ( ctx ) . Where ( opts . toConds ( ) ) . Count ( new ( Project ) )
}
// FindProjects returns a list of all projects that have been created in the repository
func FindProjects ( ctx context . Context , opts SearchOptions ) ( [ ] * Project , int64 , error ) {
e := db . GetEngine ( ctx )
projects := make ( [ ] * Project , 0 , setting . UI . IssuePagingNum )
cond := opts . toConds ( )
2020-08-16 22:07:38 -05:00
count , err := e . Where ( cond ) . Count ( new ( Project ) )
if err != nil {
2022-10-24 14:29:17 -05:00
return nil , 0 , fmt . Errorf ( "Count: %w" , err )
2020-08-16 22:07:38 -05:00
}
e = e . Where ( cond )
if opts . Page > 0 {
e = e . Limit ( setting . UI . IssuePagingNum , ( opts . Page - 1 ) * setting . UI . IssuePagingNum )
}
switch opts . SortType {
case "oldest" :
e . Desc ( "created_unix" )
case "recentupdate" :
e . Desc ( "updated_unix" )
case "leastupdate" :
e . Asc ( "updated_unix" )
default :
e . Asc ( "created_unix" )
}
return projects , count , e . Find ( & projects )
}
// NewProject creates a new Project
func NewProject ( p * Project ) error {
2022-03-29 09:16:31 -05:00
if ! IsBoardTypeValid ( p . BoardType ) {
p . BoardType = BoardTypeNone
2020-08-16 22:07:38 -05:00
}
2022-03-29 09:16:31 -05:00
if ! IsTypeValid ( p . Type ) {
2022-12-31 06:49:37 -05:00
return util . NewInvalidArgumentErrorf ( "project type is not valid" )
2020-08-16 22:07:38 -05:00
}
2022-11-12 15:18:50 -05:00
ctx , committer , err := db . TxContext ( db . DefaultContext )
2021-11-21 10:41:00 -05:00
if err != nil {
2020-08-16 22:07:38 -05:00
return err
}
2021-11-21 10:41:00 -05:00
defer committer . Close ( )
2020-08-16 22:07:38 -05:00
2021-11-21 10:41:00 -05:00
if err := db . Insert ( ctx , p ) ; err != nil {
2020-08-16 22:07:38 -05:00
return err
}
2023-01-20 06:42:33 -05:00
if p . RepoID > 0 {
if _ , err := db . Exec ( ctx , "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?" , p . RepoID ) ; err != nil {
return err
}
2020-08-16 22:07:38 -05:00
}
2022-03-29 09:16:31 -05:00
if err := createBoardsForProjectsType ( ctx , p ) ; err != nil {
2020-08-16 22:07:38 -05:00
return err
}
2021-11-21 10:41:00 -05:00
return committer . Commit ( )
2020-08-16 22:07:38 -05:00
}
// GetProjectByID returns the projects in a repository
2022-05-20 09:08:52 -05:00
func GetProjectByID ( ctx context . Context , id int64 ) ( * Project , error ) {
2020-08-16 22:07:38 -05:00
p := new ( Project )
2022-05-20 09:08:52 -05:00
has , err := db . GetEngine ( ctx ) . ID ( id ) . Get ( p )
2020-08-16 22:07:38 -05:00
if err != nil {
return nil , err
} else if ! has {
return nil , ErrProjectNotExist { ID : id }
}
return p , nil
}
// UpdateProject updates project properties
2022-05-20 09:08:52 -05:00
func UpdateProject ( ctx context . Context , p * Project ) error {
_ , err := db . GetEngine ( ctx ) . ID ( p . ID ) . Cols (
2020-08-16 22:07:38 -05:00
"title" ,
"description" ,
) . Update ( p )
return err
}
2022-05-20 09:08:52 -05:00
func updateRepositoryProjectCount ( ctx context . Context , repoID int64 ) error {
if _ , err := db . GetEngine ( ctx ) . Exec ( builder . Update (
2020-08-16 22:07:38 -05:00
builder . Eq {
"`num_projects`" : builder . Select ( "count(*)" ) . From ( "`project`" ) .
Where ( builder . Eq { "`project`.`repo_id`" : repoID } .
2022-03-29 09:16:31 -05:00
And ( builder . Eq { "`project`.`type`" : TypeRepository } ) ) ,
2020-08-16 22:07:38 -05:00
} ) . From ( "`repository`" ) . Where ( builder . Eq { "id" : repoID } ) ) ; err != nil {
return err
}
2022-05-20 09:08:52 -05:00
if _ , err := db . GetEngine ( ctx ) . Exec ( builder . Update (
2020-08-16 22:07:38 -05:00
builder . Eq {
"`num_closed_projects`" : builder . Select ( "count(*)" ) . From ( "`project`" ) .
Where ( builder . Eq { "`project`.`repo_id`" : repoID } .
2022-03-29 09:16:31 -05:00
And ( builder . Eq { "`project`.`type`" : TypeRepository } ) .
2020-08-16 22:07:38 -05:00
And ( builder . Eq { "`project`.`is_closed`" : true } ) ) ,
} ) . From ( "`repository`" ) . Where ( builder . Eq { "id" : repoID } ) ) ; err != nil {
return err
}
return nil
}
// ChangeProjectStatusByRepoIDAndID toggles a project between opened and closed
func ChangeProjectStatusByRepoIDAndID ( repoID , projectID int64 , isClosed bool ) error {
2022-11-12 15:18:50 -05:00
ctx , committer , err := db . TxContext ( db . DefaultContext )
2021-11-21 10:41:00 -05:00
if err != nil {
2020-08-16 22:07:38 -05:00
return err
}
2021-11-21 10:41:00 -05:00
defer committer . Close ( )
2020-08-16 22:07:38 -05:00
p := new ( Project )
2022-03-29 09:16:31 -05:00
has , err := db . GetEngine ( ctx ) . ID ( projectID ) . Where ( "repo_id = ?" , repoID ) . Get ( p )
2020-08-16 22:07:38 -05:00
if err != nil {
return err
} else if ! has {
return ErrProjectNotExist { ID : projectID , RepoID : repoID }
}
2022-03-29 09:16:31 -05:00
if err := changeProjectStatus ( ctx , p , isClosed ) ; err != nil {
2020-08-16 22:07:38 -05:00
return err
}
2021-11-21 10:41:00 -05:00
return committer . Commit ( )
2020-08-16 22:07:38 -05:00
}
// ChangeProjectStatus toggle a project between opened and closed
func ChangeProjectStatus ( p * Project , isClosed bool ) error {
2022-11-12 15:18:50 -05:00
ctx , committer , err := db . TxContext ( db . DefaultContext )
2021-11-21 10:41:00 -05:00
if err != nil {
2020-08-16 22:07:38 -05:00
return err
}
2021-11-21 10:41:00 -05:00
defer committer . Close ( )
2020-08-16 22:07:38 -05:00
2022-03-29 09:16:31 -05:00
if err := changeProjectStatus ( ctx , p , isClosed ) ; err != nil {
2020-08-16 22:07:38 -05:00
return err
}
2021-11-21 10:41:00 -05:00
return committer . Commit ( )
2020-08-16 22:07:38 -05:00
}
2022-03-29 09:16:31 -05:00
func changeProjectStatus ( ctx context . Context , p * Project , isClosed bool ) error {
2020-08-16 22:07:38 -05:00
p . IsClosed = isClosed
p . ClosedDateUnix = timeutil . TimeStampNow ( )
2022-05-20 09:08:52 -05:00
count , err := db . GetEngine ( ctx ) . ID ( p . ID ) . Where ( "repo_id = ? AND is_closed = ?" , p . RepoID , ! isClosed ) . Cols ( "is_closed" , "closed_date_unix" ) . Update ( p )
2020-08-16 22:07:38 -05:00
if err != nil {
return err
}
if count < 1 {
return nil
}
2022-05-20 09:08:52 -05:00
return updateRepositoryProjectCount ( ctx , p . RepoID )
2020-08-16 22:07:38 -05:00
}
2022-12-09 21:46:31 -05:00
// DeleteProjectByID deletes a project from a repository. if it's not in a database
// transaction, it will start a new database transaction
func DeleteProjectByID ( ctx context . Context , id int64 ) error {
2023-01-07 20:34:58 -05:00
return db . WithTx ( ctx , func ( ctx context . Context ) error {
2022-12-09 21:46:31 -05:00
p , err := GetProjectByID ( ctx , id )
if err != nil {
if IsErrProjectNotExist ( err ) {
return nil
}
return err
2020-08-16 22:07:38 -05:00
}
2022-12-09 21:46:31 -05:00
if err := deleteProjectIssuesByProjectID ( ctx , id ) ; err != nil {
return err
}
2020-08-16 22:07:38 -05:00
2022-12-09 21:46:31 -05:00
if err := deleteBoardByProjectID ( ctx , id ) ; err != nil {
return err
}
2020-08-16 22:07:38 -05:00
2022-12-09 21:46:31 -05:00
if _ , err = db . GetEngine ( ctx ) . ID ( p . ID ) . Delete ( new ( Project ) ) ; err != nil {
return err
}
2020-08-16 22:07:38 -05:00
2022-12-09 21:46:31 -05:00
return updateRepositoryProjectCount ( ctx , p . RepoID )
} )
2020-08-16 22:07:38 -05:00
}
2022-07-14 02:22:09 -05:00
2022-12-02 21:48:26 -05:00
func DeleteProjectByRepoID ( ctx context . Context , repoID int64 ) error {
2022-07-14 02:22:09 -05:00
switch {
case setting . Database . UseSQLite3 :
if _ , err := db . GetEngine ( ctx ) . Exec ( "DELETE FROM project_issue WHERE project_issue.id IN (SELECT project_issue.id FROM project_issue INNER JOIN project WHERE project.id = project_issue.project_id AND project.repo_id = ?)" , repoID ) ; err != nil {
return err
}
if _ , err := db . GetEngine ( ctx ) . Exec ( "DELETE FROM project_board WHERE project_board.id IN (SELECT project_board.id FROM project_board INNER JOIN project WHERE project.id = project_board.project_id AND project.repo_id = ?)" , repoID ) ; err != nil {
return err
}
if _ , err := db . GetEngine ( ctx ) . Table ( "project" ) . Where ( "repo_id = ? " , repoID ) . Delete ( & Project { } ) ; err != nil {
return err
}
case setting . Database . UsePostgreSQL :
if _ , err := db . GetEngine ( ctx ) . Exec ( "DELETE FROM project_issue USING project WHERE project.id = project_issue.project_id AND project.repo_id = ? " , repoID ) ; err != nil {
return err
}
if _ , err := db . GetEngine ( ctx ) . Exec ( "DELETE FROM project_board USING project WHERE project.id = project_board.project_id AND project.repo_id = ? " , repoID ) ; err != nil {
return err
}
if _ , err := db . GetEngine ( ctx ) . Table ( "project" ) . Where ( "repo_id = ? " , repoID ) . Delete ( & Project { } ) ; err != nil {
return err
}
default :
if _ , err := db . GetEngine ( ctx ) . Exec ( "DELETE project_issue FROM project_issue INNER JOIN project ON project.id = project_issue.project_id WHERE project.repo_id = ? " , repoID ) ; err != nil {
return err
}
if _ , err := db . GetEngine ( ctx ) . Exec ( "DELETE project_board FROM project_board INNER JOIN project ON project.id = project_board.project_id WHERE project.repo_id = ? " , repoID ) ; err != nil {
return err
}
if _ , err := db . GetEngine ( ctx ) . Table ( "project" ) . Where ( "repo_id = ? " , repoID ) . Delete ( & Project { } ) ; err != nil {
return err
}
}
return updateRepositoryProjectCount ( ctx , repoID )
}