2017-02-09 07:39:26 +01:00
// Copyright 2017 The Gitea Authors. All rights reserved.
2022-11-27 19:20:29 +01:00
// SPDX-License-Identifier: MIT
2017-02-09 07:39:26 +01:00
2022-04-08 11:11:15 +02:00
package issues
2017-02-09 07:39:26 +01:00
import (
2021-11-19 14:39:57 +01:00
"context"
2018-05-16 16:01:55 +02:00
"fmt"
2024-03-01 08:11:51 +01:00
"html/template"
2020-01-27 17:23:21 +01:00
"strings"
2018-05-16 16:01:55 +02:00
2021-09-19 13:49:59 +02:00
"code.gitea.io/gitea/models/db"
2021-12-10 02:27:50 +01:00
repo_model "code.gitea.io/gitea/models/repo"
2024-03-02 16:42:31 +01:00
"code.gitea.io/gitea/modules/optional"
2019-05-11 12:21:34 +02:00
api "code.gitea.io/gitea/modules/structs"
2019-08-15 16:46:21 +02:00
"code.gitea.io/gitea/modules/timeutil"
2022-10-18 07:50:37 +02:00
"code.gitea.io/gitea/modules/util"
2019-08-15 16:46:21 +02:00
2019-12-27 21:30:58 +01:00
"xorm.io/builder"
2017-02-09 07:39:26 +01:00
)
2022-04-08 11:11:15 +02:00
// ErrMilestoneNotExist represents a "MilestoneNotExist" kind of error.
type ErrMilestoneNotExist struct {
ID int64
RepoID int64
Name string
}
// IsErrMilestoneNotExist checks if an error is a ErrMilestoneNotExist.
func IsErrMilestoneNotExist ( err error ) bool {
_ , ok := err . ( ErrMilestoneNotExist )
return ok
}
func ( err ErrMilestoneNotExist ) Error ( ) string {
if len ( err . Name ) > 0 {
return fmt . Sprintf ( "milestone does not exist [name: %s, repo_id: %d]" , err . Name , err . RepoID )
}
return fmt . Sprintf ( "milestone does not exist [id: %d, repo_id: %d]" , err . ID , err . RepoID )
}
2022-10-18 07:50:37 +02:00
func ( err ErrMilestoneNotExist ) Unwrap ( ) error {
return util . ErrNotExist
}
2017-02-09 07:39:26 +01:00
// Milestone represents a milestone of repository.
type Milestone struct {
2021-12-10 02:27:50 +01:00
ID int64 ` xorm:"pk autoincr" `
RepoID int64 ` xorm:"INDEX" `
Repo * repo_model . Repository ` xorm:"-" `
2017-02-09 07:39:26 +01:00
Name string
2024-03-01 08:11:51 +01:00
Content string ` xorm:"TEXT" `
RenderedContent template . HTML ` xorm:"-" `
2017-02-09 07:39:26 +01:00
IsClosed bool
NumIssues int
NumClosedIssues int
NumOpenIssues int ` xorm:"-" `
Completeness int // Percentage(1-100).
2018-05-07 11:50:27 +02:00
IsOverdue bool ` xorm:"-" `
2017-02-09 07:39:26 +01:00
2020-09-05 19:38:54 +02:00
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX created" `
UpdatedUnix timeutil . TimeStamp ` xorm:"INDEX updated" `
2019-08-15 16:46:21 +02:00
DeadlineUnix timeutil . TimeStamp
ClosedDateUnix timeutil . TimeStamp
2020-09-05 19:38:54 +02:00
DeadlineString string ` xorm:"-" `
2018-04-29 07:58:47 +02:00
TotalTrackedTime int64 ` xorm:"-" `
2017-02-09 07:39:26 +01:00
}
2021-09-19 13:49:59 +02:00
func init ( ) {
db . RegisterModel ( new ( Milestone ) )
}
2017-02-09 07:39:26 +01:00
// BeforeUpdate is invoked from XORM before updating this object.
func ( m * Milestone ) BeforeUpdate ( ) {
if m . NumIssues > 0 {
m . Completeness = m . NumClosedIssues * 100 / m . NumIssues
} else {
m . Completeness = 0
}
}
2017-10-01 18:52:35 +02:00
// AfterLoad is invoked from XORM after setting the value of a field of
2017-02-09 07:39:26 +01:00
// this object.
2017-10-01 18:52:35 +02:00
func ( m * Milestone ) AfterLoad ( ) {
m . NumOpenIssues = m . NumIssues - m . NumClosedIssues
2024-11-05 07:13:19 +01:00
if m . DeadlineUnix . Year ( ) >= 9999 {
2017-10-01 18:52:35 +02:00
return
}
2017-02-09 07:39:26 +01:00
2023-12-28 11:09:57 +01:00
m . DeadlineString = m . DeadlineUnix . FormatDate ( )
2021-03-08 02:55:57 +01:00
if m . IsClosed {
m . IsOverdue = m . ClosedDateUnix >= m . DeadlineUnix
} else {
m . IsOverdue = timeutil . TimeStampNow ( ) >= m . DeadlineUnix
2017-02-09 07:39:26 +01:00
}
}
// State returns string representation of milestone status.
func ( m * Milestone ) State ( ) api . StateType {
if m . IsClosed {
return api . StateClosed
}
return api . StateOpen
}
// NewMilestone creates new milestone of repository.
2023-09-16 16:39:12 +02:00
func NewMilestone ( ctx context . Context , m * Milestone ) ( err error ) {
ctx , committer , err := db . TxContext ( ctx )
2021-11-21 16:41:00 +01:00
if err != nil {
2017-02-09 07:39:26 +01:00
return err
}
2021-11-21 16:41:00 +01:00
defer committer . Close ( )
2017-02-09 07:39:26 +01:00
2020-01-27 17:23:21 +01:00
m . Name = strings . TrimSpace ( m . Name )
2021-11-21 16:41:00 +01:00
if err = db . Insert ( ctx , m ) ; err != nil {
2017-02-09 07:39:26 +01:00
return err
}
2021-11-21 16:41:00 +01:00
if _ , err = db . Exec ( ctx , "UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?" , m . RepoID ) ; err != nil {
2017-02-09 07:39:26 +01:00
return err
}
2021-11-21 16:41:00 +01:00
return committer . Commit ( )
2017-02-09 07:39:26 +01:00
}
2022-06-30 17:55:08 +02:00
// HasMilestoneByRepoID returns if the milestone exists in the repository.
func HasMilestoneByRepoID ( ctx context . Context , repoID , id int64 ) ( bool , error ) {
return db . GetEngine ( ctx ) . ID ( id ) . Where ( "repo_id=?" , repoID ) . Exist ( new ( Milestone ) )
}
2022-04-08 11:11:15 +02:00
// GetMilestoneByRepoID returns the milestone in a repository.
func GetMilestoneByRepoID ( ctx context . Context , repoID , id int64 ) ( * Milestone , error ) {
2020-04-30 06:15:39 +02:00
m := new ( Milestone )
2022-04-08 11:11:15 +02:00
has , err := db . GetEngine ( ctx ) . ID ( id ) . Where ( "repo_id=?" , repoID ) . Get ( m )
2017-02-09 07:39:26 +01:00
if err != nil {
return nil , err
} else if ! has {
2020-04-30 06:15:39 +02:00
return nil , ErrMilestoneNotExist { ID : id , RepoID : repoID }
2017-02-09 07:39:26 +01:00
}
return m , nil
}
2020-04-30 06:15:39 +02:00
// GetMilestoneByRepoIDANDName return a milestone if one exist by name and repo
2023-09-16 16:39:12 +02:00
func GetMilestoneByRepoIDANDName ( ctx context . Context , repoID int64 , name string ) ( * Milestone , error ) {
2020-04-30 06:15:39 +02:00
var mile Milestone
2023-09-16 16:39:12 +02:00
has , err := db . GetEngine ( ctx ) . Where ( "repo_id=? AND name=?" , repoID , name ) . Get ( & mile )
2020-04-30 06:15:39 +02:00
if err != nil {
return nil , err
}
if ! has {
return nil , ErrMilestoneNotExist { Name : name , RepoID : repoID }
}
return & mile , nil
}
2017-02-09 07:39:26 +01:00
// UpdateMilestone updates information of given milestone.
2023-09-16 16:39:12 +02:00
func UpdateMilestone ( ctx context . Context , m * Milestone , oldIsClosed bool ) error {
ctx , committer , err := db . TxContext ( ctx )
2021-11-21 16:41:00 +01:00
if err != nil {
2020-01-29 07:36:32 +01:00
return err
}
2021-11-21 16:41:00 +01:00
defer committer . Close ( )
2020-01-29 07:36:32 +01:00
if m . IsClosed && ! oldIsClosed {
m . ClosedDateUnix = timeutil . TimeStampNow ( )
}
2022-01-17 19:31:58 +01:00
if err := updateMilestone ( ctx , m ) ; err != nil {
2019-10-07 23:44:58 +02:00
return err
}
2020-01-29 07:36:32 +01:00
// if IsClosed changed, update milestone numbers of repository
if oldIsClosed != m . IsClosed {
2022-01-17 19:31:58 +01:00
if err := updateRepoMilestoneNum ( ctx , m . RepoID ) ; err != nil {
2020-01-29 07:36:32 +01:00
return err
}
}
2021-11-21 16:41:00 +01:00
return committer . Commit ( )
2019-10-07 23:44:58 +02:00
}
2022-01-17 19:31:58 +01:00
func updateMilestone ( ctx context . Context , m * Milestone ) error {
2020-05-12 23:54:35 +02:00
m . Name = strings . TrimSpace ( m . Name )
2022-01-17 19:31:58 +01:00
_ , err := db . GetEngine ( ctx ) . ID ( m . ID ) . AllCols ( ) . Update ( m )
2021-06-21 20:34:58 +02:00
if err != nil {
return err
}
2022-04-08 11:11:15 +02:00
return UpdateMilestoneCounters ( ctx , m . ID )
2021-06-21 20:34:58 +02:00
}
2022-04-08 11:11:15 +02:00
// UpdateMilestoneCounters calculates NumIssues, NumClosesIssues and Completeness
func UpdateMilestoneCounters ( ctx context . Context , id int64 ) error {
2022-01-17 19:31:58 +01:00
e := db . GetEngine ( ctx )
2021-06-21 20:34:58 +02:00
_ , err := e . ID ( id ) .
2020-05-12 23:54:35 +02:00
SetExpr ( "num_issues" , builder . Select ( "count(*)" ) . From ( "issue" ) . Where (
2021-06-21 20:34:58 +02:00
builder . Eq { "milestone_id" : id } ,
2020-05-12 23:54:35 +02:00
) ) .
SetExpr ( "num_closed_issues" , builder . Select ( "count(*)" ) . From ( "issue" ) . Where (
builder . Eq {
2021-06-21 20:34:58 +02:00
"milestone_id" : id ,
2020-05-12 23:54:35 +02:00
"is_closed" : true ,
} ,
) ) .
2021-06-21 20:34:58 +02:00
Update ( & Milestone { } )
if err != nil {
return err
}
_ , err = e . Exec ( "UPDATE `milestone` SET completeness=100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) WHERE id=?" ,
id ,
2019-10-07 23:44:58 +02:00
)
return err
2017-02-09 07:39:26 +01:00
}
2020-08-17 05:07:38 +02:00
// ChangeMilestoneStatusByRepoIDAndID changes a milestone open/closed status if the milestone ID is in the repo.
2023-09-16 16:39:12 +02:00
func ChangeMilestoneStatusByRepoIDAndID ( ctx context . Context , repoID , milestoneID int64 , isClosed bool ) error {
ctx , committer , err := db . TxContext ( ctx )
2021-11-21 16:41:00 +01:00
if err != nil {
2020-08-17 05:07:38 +02:00
return err
}
2021-11-21 16:41:00 +01:00
defer committer . Close ( )
2020-08-17 05:07:38 +02:00
m := & Milestone {
ID : milestoneID ,
RepoID : repoID ,
}
2022-01-17 19:31:58 +01:00
has , err := db . GetEngine ( ctx ) . ID ( milestoneID ) . Where ( "repo_id = ?" , repoID ) . Get ( m )
2020-08-17 05:07:38 +02:00
if err != nil {
return err
} else if ! has {
return ErrMilestoneNotExist { ID : milestoneID , RepoID : repoID }
}
2022-01-17 19:31:58 +01:00
if err := changeMilestoneStatus ( ctx , m , isClosed ) ; err != nil {
2020-08-17 05:07:38 +02:00
return err
}
2021-11-21 16:41:00 +01:00
return committer . Commit ( )
2020-08-17 05:07:38 +02:00
}
2017-02-09 07:39:26 +01:00
// ChangeMilestoneStatus changes the milestone open/closed status.
2023-09-16 16:39:12 +02:00
func ChangeMilestoneStatus ( ctx context . Context , m * Milestone , isClosed bool ) ( err error ) {
ctx , committer , err := db . TxContext ( ctx )
2021-11-21 16:41:00 +01:00
if err != nil {
2017-02-09 07:39:26 +01:00
return err
}
2021-11-21 16:41:00 +01:00
defer committer . Close ( )
2017-02-09 07:39:26 +01:00
2022-01-17 19:31:58 +01:00
if err := changeMilestoneStatus ( ctx , m , isClosed ) ; err != nil {
2020-08-17 05:07:38 +02:00
return err
}
2021-11-21 16:41:00 +01:00
return committer . Commit ( )
2020-08-17 05:07:38 +02:00
}
2022-01-17 19:31:58 +01:00
func changeMilestoneStatus ( ctx context . Context , m * Milestone , isClosed bool ) error {
2017-02-09 07:39:26 +01:00
m . IsClosed = isClosed
2019-10-29 03:35:50 +01:00
if isClosed {
m . ClosedDateUnix = timeutil . TimeStampNow ( )
}
2022-01-17 19:31:58 +01:00
count , err := db . GetEngine ( ctx ) . ID ( m . ID ) . Where ( "repo_id = ? AND is_closed = ?" , m . RepoID , ! isClosed ) . Cols ( "is_closed" , "closed_date_unix" ) . Update ( m )
2020-08-17 05:07:38 +02:00
if err != nil {
2017-02-09 07:39:26 +01:00
return err
}
2020-08-17 05:07:38 +02:00
if count < 1 {
return nil
2017-12-18 15:06:51 +01:00
}
2022-01-17 19:31:58 +01:00
return updateRepoMilestoneNum ( ctx , m . RepoID )
2017-02-09 07:39:26 +01:00
}
// DeleteMilestoneByRepoID deletes a milestone from a repository.
2023-09-16 16:39:12 +02:00
func DeleteMilestoneByRepoID ( ctx context . Context , repoID , id int64 ) error {
m , err := GetMilestoneByRepoID ( ctx , repoID , id )
2017-02-09 07:39:26 +01:00
if err != nil {
if IsErrMilestoneNotExist ( err ) {
return nil
}
return err
}
2023-09-16 16:39:12 +02:00
repo , err := repo_model . GetRepositoryByID ( ctx , m . RepoID )
2017-02-09 07:39:26 +01:00
if err != nil {
return err
}
2023-09-16 16:39:12 +02:00
ctx , committer , err := db . TxContext ( ctx )
2021-11-21 16:41:00 +01:00
if err != nil {
2017-02-09 07:39:26 +01:00
return err
}
2021-11-21 16:41:00 +01:00
defer committer . Close ( )
2023-12-25 21:25:29 +01:00
if _ , err = db . DeleteByID [ Milestone ] ( ctx , m . ID ) ; err != nil {
2017-02-09 07:39:26 +01:00
return err
}
2023-12-11 09:56:48 +01:00
numMilestones , err := db . Count [ Milestone ] ( ctx , FindMilestoneOptions {
2022-06-13 11:37:59 +02:00
RepoID : repo . ID ,
} )
2017-12-18 15:06:51 +01:00
if err != nil {
return err
}
2023-12-11 09:56:48 +01:00
numClosedMilestones , err := db . Count [ Milestone ] ( ctx , FindMilestoneOptions {
RepoID : repo . ID ,
2024-03-02 16:42:31 +01:00
IsClosed : optional . Some ( true ) ,
2022-06-13 11:37:59 +02:00
} )
2017-12-18 15:06:51 +01:00
if err != nil {
return err
}
repo . NumMilestones = int ( numMilestones )
repo . NumClosedMilestones = int ( numClosedMilestones )
2023-12-25 21:25:29 +01:00
if _ , err = db . GetEngine ( ctx ) . ID ( repo . ID ) . Cols ( "num_milestones, num_closed_milestones" ) . Update ( repo ) ; err != nil {
2017-02-09 07:39:26 +01:00
return err
}
2021-11-21 16:41:00 +01:00
if _ , err = db . Exec ( ctx , "UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?" , m . ID ) ; err != nil {
2017-02-09 07:39:26 +01:00
return err
}
2021-11-21 16:41:00 +01:00
return committer . Commit ( )
2017-02-09 07:39:26 +01:00
}
2019-12-15 15:20:08 +01:00
2022-01-17 19:31:58 +01:00
func updateRepoMilestoneNum ( ctx context . Context , repoID int64 ) error {
_ , err := db . GetEngine ( ctx ) . Exec ( "UPDATE `repository` SET num_milestones=(SELECT count(*) FROM milestone WHERE repo_id=?),num_closed_milestones=(SELECT count(*) FROM milestone WHERE repo_id=? AND is_closed=?) WHERE id=?" ,
2020-05-12 23:54:35 +02:00
repoID ,
repoID ,
true ,
repoID ,
)
return err
}
2023-09-16 16:39:12 +02:00
// LoadTotalTrackedTime loads the tracked time for the milestone
func ( m * Milestone ) LoadTotalTrackedTime ( ctx context . Context ) error {
2020-05-12 23:54:35 +02:00
type totalTimesByMilestone struct {
MilestoneID int64
Time int64
}
totalTime := & totalTimesByMilestone { MilestoneID : m . ID }
2022-05-20 16:08:52 +02:00
has , err := db . GetEngine ( ctx ) . Table ( "issue" ) .
2020-05-12 23:54:35 +02:00
Join ( "INNER" , "milestone" , "issue.milestone_id = milestone.id" ) .
Join ( "LEFT" , "tracked_time" , "tracked_time.issue_id = issue.id" ) .
Where ( "tracked_time.deleted = ?" , false ) .
Select ( "milestone_id, sum(time) as time" ) .
Where ( "milestone_id = ?" , m . ID ) .
GroupBy ( "milestone_id" ) .
Get ( totalTime )
if err != nil {
return err
} else if ! has {
return nil
}
m . TotalTrackedTime = totalTime . Time
return nil
}
2023-09-08 23:09:23 +02:00
// InsertMilestones creates milestones of repository.
2023-09-16 16:39:12 +02:00
func InsertMilestones ( ctx context . Context , ms ... * Milestone ) ( err error ) {
2023-09-08 23:09:23 +02:00
if len ( ms ) == 0 {
return nil
}
2023-09-16 16:39:12 +02:00
ctx , committer , err := db . TxContext ( ctx )
2023-09-08 23:09:23 +02:00
if err != nil {
return err
}
defer committer . Close ( )
sess := db . GetEngine ( ctx )
// to return the id, so we should not use batch insert
for _ , m := range ms {
if _ , err = sess . NoAutoTime ( ) . Insert ( m ) ; err != nil {
return err
}
}
if _ , err = db . Exec ( ctx , "UPDATE `repository` SET num_milestones = num_milestones + ? WHERE id = ?" , len ( ms ) , ms [ 0 ] . RepoID ) ; err != nil {
return err
}
return committer . Commit ( )
}