2014-03-20 16:04:56 -04:00
|
|
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
2020-01-01 23:51:10 +01:00
|
|
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
2022-11-27 13:20:29 -05:00
|
|
|
// SPDX-License-Identifier: MIT
|
2014-03-20 16:04:56 -04:00
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
package issues
|
2014-03-20 16:04:56 -04:00
|
|
|
|
2014-03-22 13:50:50 -04:00
|
|
|
import (
|
2021-09-23 16:45:36 +01:00
|
|
|
"context"
|
2015-08-10 14:42:50 +08:00
|
|
|
"fmt"
|
2018-01-03 09:34:13 +01:00
|
|
|
"regexp"
|
2014-05-07 16:51:14 -04:00
|
|
|
|
2021-09-19 19:49:59 +08:00
|
|
|
"code.gitea.io/gitea/models/db"
|
2022-03-29 22:16:31 +08:00
|
|
|
project_model "code.gitea.io/gitea/models/project"
|
2021-11-19 21:39:57 +08:00
|
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
2021-11-24 17:49:20 +08:00
|
|
|
user_model "code.gitea.io/gitea/models/user"
|
2016-11-10 17:24:48 +01:00
|
|
|
"code.gitea.io/gitea/modules/log"
|
2019-05-11 18:21:34 +08:00
|
|
|
api "code.gitea.io/gitea/modules/structs"
|
2019-08-15 22:46:21 +08:00
|
|
|
"code.gitea.io/gitea/modules/timeutil"
|
2017-01-24 21:43:02 -05:00
|
|
|
"code.gitea.io/gitea/modules/util"
|
2017-12-25 18:25:16 -05:00
|
|
|
|
2019-06-23 23:22:43 +08:00
|
|
|
"xorm.io/builder"
|
2014-03-22 13:50:50 -04:00
|
|
|
)
|
|
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
// ErrIssueNotExist represents a "IssueNotExist" kind of error.
|
|
|
|
|
type ErrIssueNotExist struct {
|
|
|
|
|
ID int64
|
|
|
|
|
RepoID int64
|
|
|
|
|
Index int64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsErrIssueNotExist checks if an error is a ErrIssueNotExist.
|
|
|
|
|
func IsErrIssueNotExist(err error) bool {
|
|
|
|
|
_, ok := err.(ErrIssueNotExist)
|
|
|
|
|
return ok
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (err ErrIssueNotExist) Error() string {
|
|
|
|
|
return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index)
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-18 06:50:37 +01:00
|
|
|
func (err ErrIssueNotExist) Unwrap() error {
|
|
|
|
|
return util.ErrNotExist
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
// ErrIssueIsClosed represents a "IssueIsClosed" kind of error.
|
|
|
|
|
type ErrIssueIsClosed struct {
|
|
|
|
|
ID int64
|
|
|
|
|
RepoID int64
|
|
|
|
|
Index int64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsErrIssueIsClosed checks if an error is a ErrIssueNotExist.
|
|
|
|
|
func IsErrIssueIsClosed(err error) bool {
|
|
|
|
|
_, ok := err.(ErrIssueIsClosed)
|
|
|
|
|
return ok
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (err ErrIssueIsClosed) Error() string {
|
|
|
|
|
return fmt.Sprintf("issue is closed [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ErrNewIssueInsert is used when the INSERT statement in newIssue fails
|
|
|
|
|
type ErrNewIssueInsert struct {
|
|
|
|
|
OriginalError error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsErrNewIssueInsert checks if an error is a ErrNewIssueInsert.
|
|
|
|
|
func IsErrNewIssueInsert(err error) bool {
|
|
|
|
|
_, ok := err.(ErrNewIssueInsert)
|
|
|
|
|
return ok
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (err ErrNewIssueInsert) Error() string {
|
|
|
|
|
return err.OriginalError.Error()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ErrIssueWasClosed is used when close a closed issue
|
|
|
|
|
type ErrIssueWasClosed struct {
|
|
|
|
|
ID int64
|
|
|
|
|
Index int64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsErrIssueWasClosed checks if an error is a ErrIssueWasClosed.
|
|
|
|
|
func IsErrIssueWasClosed(err error) bool {
|
|
|
|
|
_, ok := err.(ErrIssueWasClosed)
|
|
|
|
|
return ok
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (err ErrIssueWasClosed) Error() string {
|
|
|
|
|
return fmt.Sprintf("Issue [%d] %d was already closed", err.ID, err.Index)
|
|
|
|
|
}
|
|
|
|
|
|
2014-03-22 13:50:50 -04:00
|
|
|
// Issue represents an issue or pull request of repository.
|
2014-03-20 16:04:56 -04:00
|
|
|
type Issue struct {
|
2021-12-10 09:27:50 +08:00
|
|
|
ID int64 `xorm:"pk autoincr"`
|
|
|
|
|
RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
|
|
|
|
|
Repo *repo_model.Repository `xorm:"-"`
|
|
|
|
|
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
|
|
|
|
|
PosterID int64 `xorm:"INDEX"`
|
|
|
|
|
Poster *user_model.User `xorm:"-"`
|
2019-07-07 22:14:12 -04:00
|
|
|
OriginalAuthor string
|
2022-06-13 17:37:59 +08:00
|
|
|
OriginalAuthorID int64 `xorm:"index"`
|
|
|
|
|
Title string `xorm:"name"`
|
|
|
|
|
Content string `xorm:"LONGTEXT"`
|
|
|
|
|
RenderedContent string `xorm:"-"`
|
|
|
|
|
Labels []*Label `xorm:"-"`
|
|
|
|
|
MilestoneID int64 `xorm:"INDEX"`
|
|
|
|
|
Milestone *Milestone `xorm:"-"`
|
|
|
|
|
Project *project_model.Project `xorm:"-"`
|
2019-07-07 22:14:12 -04:00
|
|
|
Priority int
|
2021-11-24 17:49:20 +08:00
|
|
|
AssigneeID int64 `xorm:"-"`
|
|
|
|
|
Assignee *user_model.User `xorm:"-"`
|
|
|
|
|
IsClosed bool `xorm:"INDEX"`
|
|
|
|
|
IsRead bool `xorm:"-"`
|
|
|
|
|
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
|
|
|
|
PullRequest *PullRequest `xorm:"-"`
|
2019-07-07 22:14:12 -04:00
|
|
|
NumComments int
|
|
|
|
|
Ref string
|
2016-03-09 19:53:30 -05:00
|
|
|
|
2019-08-15 22:46:21 +08:00
|
|
|
DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`
|
2018-05-01 21:05:28 +02:00
|
|
|
|
2019-08-15 22:46:21 +08:00
|
|
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
|
|
|
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
|
|
|
|
ClosedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
2015-08-12 17:04:23 +08:00
|
|
|
|
2022-12-23 19:35:43 +08:00
|
|
|
Attachments []*repo_model.Attachment `xorm:"-"`
|
|
|
|
|
Comments []*Comment `xorm:"-"`
|
|
|
|
|
Reactions ReactionList `xorm:"-"`
|
|
|
|
|
TotalTrackedTime int64 `xorm:"-"`
|
|
|
|
|
Assignees []*user_model.User `xorm:"-"`
|
2019-02-18 21:55:04 +01:00
|
|
|
|
|
|
|
|
// IsLocked limits commenting abilities to users on an issue
|
|
|
|
|
// with write access
|
|
|
|
|
IsLocked bool `xorm:"NOT NULL DEFAULT false"`
|
2020-09-11 02:09:14 +08:00
|
|
|
|
|
|
|
|
// For view issue page.
|
2021-11-11 07:29:30 +01:00
|
|
|
ShowRole RoleDescriptor `xorm:"-"`
|
2015-08-12 17:04:23 +08:00
|
|
|
}
|
|
|
|
|
|
2018-01-03 09:34:13 +01:00
|
|
|
var (
|
|
|
|
|
issueTasksPat *regexp.Regexp
|
|
|
|
|
issueTasksDonePat *regexp.Regexp
|
|
|
|
|
)
|
|
|
|
|
|
2021-03-15 02:52:12 +08:00
|
|
|
const (
|
2021-06-14 10:22:55 +08:00
|
|
|
issueTasksRegexpStr = `(^\s*[-*]\s\[[\sxX]\]\s.)|(\n\s*[-*]\s\[[\sxX]\]\s.)`
|
|
|
|
|
issueTasksDoneRegexpStr = `(^\s*[-*]\s\[[xX]\]\s.)|(\n\s*[-*]\s\[[xX]\]\s.)`
|
2021-03-15 02:52:12 +08:00
|
|
|
)
|
2018-01-03 09:34:13 +01:00
|
|
|
|
2021-09-19 19:49:59 +08:00
|
|
|
// IssueIndex represents the issue index table
|
|
|
|
|
type IssueIndex db.ResourceIndex
|
|
|
|
|
|
2018-01-03 09:34:13 +01:00
|
|
|
func init() {
|
|
|
|
|
issueTasksPat = regexp.MustCompile(issueTasksRegexpStr)
|
|
|
|
|
issueTasksDonePat = regexp.MustCompile(issueTasksDoneRegexpStr)
|
2021-09-19 19:49:59 +08:00
|
|
|
|
|
|
|
|
db.RegisterModel(new(Issue))
|
|
|
|
|
db.RegisterModel(new(IssueIndex))
|
2018-01-03 09:34:13 +01:00
|
|
|
}
|
|
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
// LoadTotalTimes load total tracked time
|
|
|
|
|
func (issue *Issue) LoadTotalTimes(ctx context.Context) (err error) {
|
2018-04-29 07:58:47 +02:00
|
|
|
opts := FindTrackedTimesOptions{IssueID: issue.ID}
|
2022-05-20 22:08:52 +08:00
|
|
|
issue.TotalTrackedTime, err = opts.toSession(db.GetEngine(ctx)).SumInt(&TrackedTime{}, "time")
|
2018-04-29 07:58:47 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-01 21:05:28 +02:00
|
|
|
// IsOverdue checks if the issue is overdue
|
|
|
|
|
func (issue *Issue) IsOverdue() bool {
|
2021-03-08 01:55:57 +00:00
|
|
|
if issue.IsClosed {
|
|
|
|
|
return issue.ClosedUnix >= issue.DeadlineUnix
|
|
|
|
|
}
|
2019-08-15 22:46:21 +08:00
|
|
|
return timeutil.TimeStampNow() >= issue.DeadlineUnix
|
2018-05-01 21:05:28 +02:00
|
|
|
}
|
|
|
|
|
|
2018-12-13 23:55:43 +08:00
|
|
|
// LoadRepo loads issue's repository
|
2022-04-08 17:11:15 +08:00
|
|
|
func (issue *Issue) LoadRepo(ctx context.Context) (err error) {
|
2023-03-28 19:23:25 +02:00
|
|
|
if issue.Repo == nil && issue.RepoID != 0 {
|
2022-12-03 10:48:26 +08:00
|
|
|
issue.Repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID)
|
2016-03-13 23:20:22 -04:00
|
|
|
if err != nil {
|
2022-10-24 21:29:17 +02:00
|
|
|
return fmt.Errorf("getRepositoryByID [%d]: %w", issue.RepoID, err)
|
2016-03-13 23:20:22 -04:00
|
|
|
}
|
2016-08-26 13:40:53 -07:00
|
|
|
}
|
2016-12-17 19:49:17 +08:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-29 07:58:47 +02:00
|
|
|
// IsTimetrackerEnabled returns true if the repo enables timetracking
|
2022-12-10 10:46:31 +08:00
|
|
|
func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
|
2022-04-08 17:11:15 +08:00
|
|
|
if err := issue.LoadRepo(ctx); err != nil {
|
2019-04-02 08:48:31 +01:00
|
|
|
log.Error(fmt.Sprintf("loadRepo: %v", err))
|
2018-04-29 07:58:47 +02:00
|
|
|
return false
|
|
|
|
|
}
|
2022-12-10 10:46:31 +08:00
|
|
|
return issue.Repo.IsTimetrackerEnabled(ctx)
|
2018-04-29 07:58:47 +02:00
|
|
|
}
|
|
|
|
|
|
2017-01-28 14:01:07 -02:00
|
|
|
// GetPullRequest returns the issue pull request
|
|
|
|
|
func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) {
|
|
|
|
|
if !issue.IsPull {
|
|
|
|
|
return nil, fmt.Errorf("Issue is not a pull request")
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-20 22:08:52 +08:00
|
|
|
pr, err = GetPullRequestByIssueID(db.DefaultContext, issue.ID)
|
2018-10-18 19:23:05 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
pr.Issue = issue
|
2022-06-20 12:02:49 +02:00
|
|
|
return pr, err
|
2017-01-28 14:01:07 -02:00
|
|
|
}
|
|
|
|
|
|
2018-12-13 23:55:43 +08:00
|
|
|
// LoadPoster loads poster
|
2022-11-19 09:12:33 +01:00
|
|
|
func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
|
2023-03-28 19:23:25 +02:00
|
|
|
if issue.Poster == nil && issue.PosterID != 0 {
|
2023-01-31 09:45:19 +08:00
|
|
|
issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID)
|
2016-03-13 23:20:22 -04:00
|
|
|
if err != nil {
|
2016-09-20 17:54:47 +08:00
|
|
|
issue.PosterID = -1
|
2021-11-24 17:49:20 +08:00
|
|
|
issue.Poster = user_model.NewGhostUser()
|
|
|
|
|
if !user_model.IsErrUserNotExist(err) {
|
2022-10-24 21:29:17 +02:00
|
|
|
return fmt.Errorf("getUserByID.(poster) [%d]: %w", issue.PosterID, err)
|
2016-03-13 23:20:22 -04:00
|
|
|
}
|
2016-11-09 13:07:01 +08:00
|
|
|
err = nil
|
2016-03-13 23:20:22 -04:00
|
|
|
return
|
|
|
|
|
}
|
2016-08-26 13:40:53 -07:00
|
|
|
}
|
2022-06-20 12:02:49 +02:00
|
|
|
return err
|
2017-01-30 20:46:45 +08:00
|
|
|
}
|
2016-03-13 23:20:22 -04:00
|
|
|
|
2022-11-19 09:12:33 +01:00
|
|
|
// LoadPullRequest loads pull request info
|
|
|
|
|
func (issue *Issue) LoadPullRequest(ctx context.Context) (err error) {
|
2023-02-21 00:15:49 +00:00
|
|
|
if issue.IsPull {
|
2023-03-28 19:23:25 +02:00
|
|
|
if issue.PullRequest == nil && issue.ID != 0 {
|
2023-02-21 00:15:49 +00:00
|
|
|
issue.PullRequest, err = GetPullRequestByIssueID(ctx, issue.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if IsErrPullRequestNotExist(err) {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("getPullRequestByIssueID [%d]: %w", issue.ID, err)
|
2017-07-26 00:16:45 -07:00
|
|
|
}
|
|
|
|
|
}
|
2023-03-28 19:23:25 +02:00
|
|
|
if issue.PullRequest != nil {
|
|
|
|
|
issue.PullRequest.Issue = issue
|
|
|
|
|
}
|
2017-07-26 00:16:45 -07:00
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-20 22:08:52 +08:00
|
|
|
func (issue *Issue) loadComments(ctx context.Context) (err error) {
|
2023-04-20 14:39:44 +08:00
|
|
|
return issue.loadCommentsByType(ctx, CommentTypeUndefined)
|
2019-02-19 22:39:39 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LoadDiscussComments loads discuss comments
|
2022-11-19 09:12:33 +01:00
|
|
|
func (issue *Issue) LoadDiscussComments(ctx context.Context) error {
|
|
|
|
|
return issue.loadCommentsByType(ctx, CommentTypeComment)
|
2019-02-19 22:39:39 +08:00
|
|
|
}
|
|
|
|
|
|
2022-05-20 22:08:52 +08:00
|
|
|
func (issue *Issue) loadCommentsByType(ctx context.Context, tp CommentType) (err error) {
|
2017-09-16 13:16:21 -07:00
|
|
|
if issue.Comments != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2022-05-20 22:08:52 +08:00
|
|
|
issue.Comments, err = FindComments(ctx, &FindCommentsOptions{
|
2017-09-16 13:16:21 -07:00
|
|
|
IssueID: issue.ID,
|
2019-02-19 22:39:39 +08:00
|
|
|
Type: tp,
|
2017-09-16 13:16:21 -07:00
|
|
|
})
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-10 09:27:50 +08:00
|
|
|
func (issue *Issue) loadReactions(ctx context.Context) (err error) {
|
2017-12-04 01:14:26 +02:00
|
|
|
if issue.Reactions != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2022-06-13 17:37:59 +08:00
|
|
|
reactions, _, err := FindReactions(ctx, FindReactionsOptions{
|
2017-12-04 01:14:26 +02:00
|
|
|
IssueID: issue.ID,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2022-04-08 17:11:15 +08:00
|
|
|
if err = issue.LoadRepo(ctx); err != nil {
|
2020-01-15 19:14:07 +08:00
|
|
|
return err
|
|
|
|
|
}
|
2017-12-04 01:14:26 +02:00
|
|
|
// Load reaction user data
|
2022-06-20 12:02:49 +02:00
|
|
|
if _, err := reactions.LoadUsers(ctx, issue.Repo); err != nil {
|
2017-12-04 01:14:26 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cache comments to map
|
|
|
|
|
comments := make(map[int64]*Comment)
|
|
|
|
|
for _, comment := range issue.Comments {
|
|
|
|
|
comments[comment.ID] = comment
|
|
|
|
|
}
|
|
|
|
|
// Add reactions either to issue or comment
|
|
|
|
|
for _, react := range reactions {
|
|
|
|
|
if react.CommentID == 0 {
|
|
|
|
|
issue.Reactions = append(issue.Reactions, react)
|
|
|
|
|
} else if comment, ok := comments[react.CommentID]; ok {
|
|
|
|
|
comment.Reactions = append(comment.Reactions, react)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-19 09:12:33 +01:00
|
|
|
// LoadMilestone load milestone of this issue.
|
|
|
|
|
func (issue *Issue) LoadMilestone(ctx context.Context) (err error) {
|
2020-05-24 22:38:34 +08:00
|
|
|
if (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {
|
2022-06-13 17:37:59 +08:00
|
|
|
issue.Milestone, err = GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID)
|
|
|
|
|
if err != nil && !IsErrMilestoneNotExist(err) {
|
2022-10-24 21:29:17 +02:00
|
|
|
return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %w", issue.RepoID, issue.MilestoneID, err)
|
2020-01-01 23:51:10 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
// LoadAttributes loads the attribute of this issue.
|
|
|
|
|
func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
|
2022-04-08 17:11:15 +08:00
|
|
|
if err = issue.LoadRepo(ctx); err != nil {
|
2017-01-30 20:46:45 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-19 09:12:33 +01:00
|
|
|
if err = issue.LoadPoster(ctx); err != nil {
|
2017-01-30 20:46:45 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-28 13:48:48 +02:00
|
|
|
if err = issue.LoadLabels(ctx); err != nil {
|
2017-01-30 20:46:45 +08:00
|
|
|
return
|
2016-08-26 13:40:53 -07:00
|
|
|
}
|
2015-08-10 21:47:23 +08:00
|
|
|
|
2022-11-19 09:12:33 +01:00
|
|
|
if err = issue.LoadMilestone(ctx); err != nil {
|
2020-01-01 23:51:10 +01:00
|
|
|
return
|
2015-08-05 20:23:08 +08:00
|
|
|
}
|
|
|
|
|
|
2023-02-21 04:21:56 +09:00
|
|
|
if err = issue.LoadProject(ctx); err != nil {
|
2020-08-17 04:07:38 +01:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-20 22:08:52 +08:00
|
|
|
if err = issue.LoadAssignees(ctx); err != nil {
|
2017-06-23 15:43:37 +02:00
|
|
|
return
|
2016-08-14 03:32:24 -07:00
|
|
|
}
|
|
|
|
|
|
2022-11-19 09:12:33 +01:00
|
|
|
if err = issue.LoadPullRequest(ctx); err != nil && !IsErrPullRequestNotExist(err) {
|
2016-08-14 03:32:24 -07:00
|
|
|
// It is possible pull request is not yet created.
|
2017-07-26 00:16:45 -07:00
|
|
|
return err
|
2016-08-14 03:32:24 -07:00
|
|
|
}
|
|
|
|
|
|
2016-08-26 13:40:53 -07:00
|
|
|
if issue.Attachments == nil {
|
2022-05-20 22:08:52 +08:00
|
|
|
issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)
|
2016-08-26 13:40:53 -07:00
|
|
|
if err != nil {
|
2022-10-24 21:29:17 +02:00
|
|
|
return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)
|
2016-08-26 13:40:53 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-20 22:08:52 +08:00
|
|
|
if err = issue.loadComments(ctx); err != nil {
|
2017-12-04 01:14:26 +02:00
|
|
|
return err
|
2016-08-26 13:40:53 -07:00
|
|
|
}
|
2019-04-18 13:00:03 +08:00
|
|
|
|
2021-12-10 09:27:50 +08:00
|
|
|
if err = CommentList(issue.Comments).loadAttributes(ctx); err != nil {
|
2019-04-18 13:00:03 +08:00
|
|
|
return err
|
|
|
|
|
}
|
2022-12-10 10:46:31 +08:00
|
|
|
if issue.IsTimetrackerEnabled(ctx) {
|
2022-06-13 17:37:59 +08:00
|
|
|
if err = issue.LoadTotalTimes(ctx); err != nil {
|
2018-04-29 07:58:47 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-08-26 13:40:53 -07:00
|
|
|
|
2021-12-10 09:27:50 +08:00
|
|
|
return issue.loadReactions(ctx)
|
2016-08-14 03:32:24 -07:00
|
|
|
}
|
|
|
|
|
|
2017-02-03 02:22:39 -05:00
|
|
|
// GetIsRead load the `IsRead` field of the issue
|
|
|
|
|
func (issue *Issue) GetIsRead(userID int64) error {
|
|
|
|
|
issueUser := &IssueUser{IssueID: issue.ID, UID: userID}
|
2021-09-23 16:45:36 +01:00
|
|
|
if has, err := db.GetEngine(db.DefaultContext).Get(issueUser); err != nil {
|
2017-02-03 02:22:39 -05:00
|
|
|
return err
|
|
|
|
|
} else if !has {
|
2017-02-08 22:47:24 -05:00
|
|
|
issue.IsRead = false
|
|
|
|
|
return nil
|
2017-02-03 02:22:39 -05:00
|
|
|
}
|
|
|
|
|
issue.IsRead = issueUser.IsRead
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-03 22:35:42 +08:00
|
|
|
// APIURL returns the absolute APIURL to this issue.
|
|
|
|
|
func (issue *Issue) APIURL() string {
|
2020-04-21 15:48:53 +02:00
|
|
|
if issue.Repo == nil {
|
2022-04-08 17:11:15 +08:00
|
|
|
err := issue.LoadRepo(db.DefaultContext)
|
2020-04-21 15:48:53 +02:00
|
|
|
if err != nil {
|
|
|
|
|
log.Error("Issue[%d].APIURL(): %v", issue.ID, err)
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-01-14 16:37:19 +01:00
|
|
|
return fmt.Sprintf("%s/issues/%d", issue.Repo.APIURL(), issue.Index)
|
2017-03-03 22:35:42 +08:00
|
|
|
}
|
|
|
|
|
|
2016-11-24 09:41:11 +01:00
|
|
|
// HTMLURL returns the absolute URL to this issue.
|
2016-08-16 10:19:09 -07:00
|
|
|
func (issue *Issue) HTMLURL() string {
|
|
|
|
|
var path string
|
|
|
|
|
if issue.IsPull {
|
|
|
|
|
path = "pulls"
|
|
|
|
|
} else {
|
|
|
|
|
path = "issues"
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(), path, issue.Index)
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-07 02:09:18 +08:00
|
|
|
// Link returns the issue's relative URL.
|
2021-11-16 18:18:25 +00:00
|
|
|
func (issue *Issue) Link() string {
|
|
|
|
|
var path string
|
|
|
|
|
if issue.IsPull {
|
|
|
|
|
path = "pulls"
|
|
|
|
|
} else {
|
|
|
|
|
path = "issues"
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf("%s/%s/%d", issue.Repo.Link(), path, issue.Index)
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-02 12:10:39 +01:00
|
|
|
// DiffURL returns the absolute URL to this diff
|
|
|
|
|
func (issue *Issue) DiffURL() string {
|
|
|
|
|
if issue.IsPull {
|
|
|
|
|
return fmt.Sprintf("%s/pulls/%d.diff", issue.Repo.HTMLURL(), issue.Index)
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PatchURL returns the absolute URL to this patch
|
|
|
|
|
func (issue *Issue) PatchURL() string {
|
|
|
|
|
if issue.IsPull {
|
|
|
|
|
return fmt.Sprintf("%s/pulls/%d.patch", issue.Repo.HTMLURL(), issue.Index)
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-13 23:20:22 -04:00
|
|
|
// State returns string representation of issue status.
|
2016-11-22 12:24:39 +01:00
|
|
|
func (issue *Issue) State() api.StateType {
|
|
|
|
|
if issue.IsClosed {
|
2016-11-29 09:25:47 +01:00
|
|
|
return api.StateClosed
|
2016-03-13 23:20:22 -04:00
|
|
|
}
|
2016-11-29 09:25:47 +01:00
|
|
|
return api.StateOpen
|
2016-08-14 03:32:24 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HashTag returns unique hash tag for issue.
|
2016-11-22 12:24:39 +01:00
|
|
|
func (issue *Issue) HashTag() string {
|
2020-12-25 09:59:32 +00:00
|
|
|
return fmt.Sprintf("issue-%d", issue.ID)
|
2016-03-13 23:20:22 -04:00
|
|
|
}
|
|
|
|
|
|
2015-08-13 16:07:11 +08:00
|
|
|
// IsPoster returns true if given user by ID is the poster.
|
2016-11-22 12:24:39 +01:00
|
|
|
func (issue *Issue) IsPoster(uid int64) bool {
|
2020-01-17 18:23:46 +08:00
|
|
|
return issue.OriginalAuthorID == 0 && issue.PosterID == uid
|
2015-08-13 16:07:11 +08:00
|
|
|
}
|
|
|
|
|
|
2018-01-03 09:34:13 +01:00
|
|
|
// GetTasks returns the amount of tasks in the issues content
|
|
|
|
|
func (issue *Issue) GetTasks() int {
|
|
|
|
|
return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetTasksDone returns the amount of completed tasks in the issues content
|
|
|
|
|
func (issue *Issue) GetTasksDone() int {
|
|
|
|
|
return len(issueTasksDonePat.FindAllStringIndex(issue.Content, -1))
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-18 18:45:25 +08:00
|
|
|
// GetLastEventTimestamp returns the last user visible event timestamp, either the creation of this issue or the close.
|
|
|
|
|
func (issue *Issue) GetLastEventTimestamp() timeutil.TimeStamp {
|
|
|
|
|
if issue.IsClosed {
|
|
|
|
|
return issue.ClosedUnix
|
2015-09-02 16:18:09 -04:00
|
|
|
}
|
2023-05-18 18:45:25 +08:00
|
|
|
return issue.CreatedUnix
|
|
|
|
|
}
|
2015-09-02 16:18:09 -04:00
|
|
|
|
2023-05-18 18:45:25 +08:00
|
|
|
// GetLastEventLabel returns the localization label for the current issue.
|
|
|
|
|
func (issue *Issue) GetLastEventLabel() string {
|
|
|
|
|
if issue.IsClosed {
|
|
|
|
|
if issue.IsPull && issue.PullRequest.HasMerged {
|
|
|
|
|
return "repo.pulls.merged_by"
|
|
|
|
|
}
|
|
|
|
|
return "repo.issues.closed_by"
|
2015-08-25 22:58:34 +08:00
|
|
|
}
|
2023-05-18 18:45:25 +08:00
|
|
|
return "repo.issues.opened_by"
|
2015-08-25 22:58:34 +08:00
|
|
|
}
|
|
|
|
|
|
2023-05-18 18:45:25 +08:00
|
|
|
// GetLastComment return last comment for the current issue.
|
|
|
|
|
func (issue *Issue) GetLastComment() (*Comment, error) {
|
|
|
|
|
var c Comment
|
|
|
|
|
exist, err := db.GetEngine(db.DefaultContext).Where("type = ?", CommentTypeComment).
|
|
|
|
|
And("issue_id = ?", issue.ID).Desc("created_unix").Get(&c)
|
2019-02-21 13:01:28 +08:00
|
|
|
if err != nil {
|
2023-05-18 18:45:25 +08:00
|
|
|
return nil, err
|
2019-02-21 13:01:28 +08:00
|
|
|
}
|
2023-05-18 18:45:25 +08:00
|
|
|
if !exist {
|
|
|
|
|
return nil, nil
|
2021-03-06 23:11:12 +08:00
|
|
|
}
|
2023-05-18 18:45:25 +08:00
|
|
|
return &c, nil
|
|
|
|
|
}
|
2019-02-21 13:01:28 +08:00
|
|
|
|
2023-05-18 18:45:25 +08:00
|
|
|
// GetLastEventLabelFake returns the localization label for the current issue without providing a link in the username.
|
|
|
|
|
func (issue *Issue) GetLastEventLabelFake() string {
|
|
|
|
|
if issue.IsClosed {
|
|
|
|
|
if issue.IsPull && issue.PullRequest.HasMerged {
|
|
|
|
|
return "repo.pulls.merged_by_fake"
|
|
|
|
|
}
|
|
|
|
|
return "repo.issues.closed_by_fake"
|
2019-02-21 13:01:28 +08:00
|
|
|
}
|
2023-05-18 18:45:25 +08:00
|
|
|
return "repo.issues.opened_by_fake"
|
2019-02-21 13:01:28 +08:00
|
|
|
}
|
|
|
|
|
|
2023-05-18 18:45:25 +08:00
|
|
|
// GetIssueByIndex returns raw issue without loading attributes by index in a repository.
|
|
|
|
|
func GetIssueByIndex(repoID, index int64) (*Issue, error) {
|
|
|
|
|
if index < 1 {
|
|
|
|
|
return nil, ErrIssueNotExist{}
|
2020-05-16 22:05:19 +01:00
|
|
|
}
|
2023-05-18 18:45:25 +08:00
|
|
|
issue := &Issue{
|
|
|
|
|
RepoID: repoID,
|
|
|
|
|
Index: index,
|
2020-05-16 22:05:19 +01:00
|
|
|
}
|
2023-05-18 18:45:25 +08:00
|
|
|
has, err := db.GetEngine(db.DefaultContext).Get(issue)
|
2020-05-16 22:05:19 +01:00
|
|
|
if err != nil {
|
2023-05-18 18:45:25 +08:00
|
|
|
return nil, err
|
|
|
|
|
} else if !has {
|
|
|
|
|
return nil, ErrIssueNotExist{0, repoID, index}
|
2020-01-02 08:54:22 +01:00
|
|
|
}
|
2023-05-18 18:45:25 +08:00
|
|
|
return issue, nil
|
|
|
|
|
}
|
2020-01-02 08:54:22 +01:00
|
|
|
|
2023-05-18 18:45:25 +08:00
|
|
|
// GetIssueWithAttrsByIndex returns issue by index in a repository.
|
|
|
|
|
func GetIssueWithAttrsByIndex(repoID, index int64) (*Issue, error) {
|
|
|
|
|
issue, err := GetIssueByIndex(repoID, index)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2019-09-20 02:45:38 -03:00
|
|
|
}
|
2023-05-18 18:45:25 +08:00
|
|
|
return issue, issue.LoadAttributes(db.DefaultContext)
|
|
|
|
|
}
|
2020-05-16 22:05:19 +01:00
|
|
|
|
2023-05-18 18:45:25 +08:00
|
|
|
// GetIssueByID returns an issue by given ID.
|
|
|
|
|
func GetIssueByID(ctx context.Context, id int64) (*Issue, error) {
|
|
|
|
|
issue := new(Issue)
|
|
|
|
|
has, err := db.GetEngine(ctx).ID(id).Get(issue)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
} else if !has {
|
|
|
|
|
return nil, ErrIssueNotExist{id, 0, 0}
|
2020-05-16 22:05:19 +01:00
|
|
|
}
|
2023-05-18 18:45:25 +08:00
|
|
|
return issue, nil
|
2015-10-24 03:36:47 -04:00
|
|
|
}
|
2018-05-01 21:05:28 +02:00
|
|
|
|
2023-05-18 18:45:25 +08:00
|
|
|
// GetIssueWithAttrsByID returns an issue with attributes by given ID.
|
|
|
|
|
func GetIssueWithAttrsByID(id int64) (*Issue, error) {
|
|
|
|
|
issue, err := GetIssueByID(db.DefaultContext, id)
|
2021-11-19 21:39:57 +08:00
|
|
|
if err != nil {
|
2023-05-18 18:45:25 +08:00
|
|
|
return nil, err
|
2018-05-01 21:05:28 +02:00
|
|
|
}
|
2023-05-18 18:45:25 +08:00
|
|
|
return issue, issue.LoadAttributes(db.DefaultContext)
|
|
|
|
|
}
|
2018-05-01 21:05:28 +02:00
|
|
|
|
2023-05-18 18:45:25 +08:00
|
|
|
// GetIssuesByIDs return issues with the given IDs.
|
|
|
|
|
func GetIssuesByIDs(ctx context.Context, issueIDs []int64) (IssueList, error) {
|
|
|
|
|
issues := make([]*Issue, 0, 10)
|
|
|
|
|
return issues, db.GetEngine(ctx).In("id", issueIDs).Find(&issues)
|
|
|
|
|
}
|
2018-05-01 21:05:28 +02:00
|
|
|
|
2023-05-18 18:45:25 +08:00
|
|
|
// GetIssueIDsByRepoID returns all issue ids by repo id
|
|
|
|
|
func GetIssueIDsByRepoID(ctx context.Context, repoID int64) ([]int64, error) {
|
|
|
|
|
ids := make([]int64, 0, 10)
|
|
|
|
|
err := db.GetEngine(ctx).Table("issue").Cols("id").Where("repo_id = ?", repoID).Find(&ids)
|
|
|
|
|
return ids, err
|
|
|
|
|
}
|
2018-05-01 21:05:28 +02:00
|
|
|
|
2023-05-18 18:45:25 +08:00
|
|
|
// GetParticipantsIDsByIssueID returns the IDs of all users who participated in comments of an issue,
|
|
|
|
|
// but skips joining with `user` for performance reasons.
|
|
|
|
|
// User permissions must be verified elsewhere if required.
|
|
|
|
|
func GetParticipantsIDsByIssueID(ctx context.Context, issueID int64) ([]int64, error) {
|
|
|
|
|
userIDs := make([]int64, 0, 5)
|
|
|
|
|
return userIDs, db.GetEngine(ctx).
|
|
|
|
|
Table("comment").
|
|
|
|
|
Cols("poster_id").
|
|
|
|
|
Where("issue_id = ?", issueID).
|
|
|
|
|
And("type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview).
|
|
|
|
|
Distinct("poster_id").
|
|
|
|
|
Find(&userIDs)
|
2018-05-01 21:05:28 +02:00
|
|
|
}
|
2018-07-17 23:23:58 +02:00
|
|
|
|
2023-05-18 18:45:25 +08:00
|
|
|
// IsUserParticipantsOfIssue return true if user is participants of an issue
|
|
|
|
|
func IsUserParticipantsOfIssue(user *user_model.User, issue *Issue) bool {
|
|
|
|
|
userIDs, err := issue.GetParticipantIDsByIssue(db.DefaultContext)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(err.Error())
|
|
|
|
|
return false
|
2022-03-01 01:20:15 +01:00
|
|
|
}
|
2023-05-18 18:45:25 +08:00
|
|
|
return util.SliceContains(userIDs, user.ID)
|
2022-03-01 01:20:15 +01:00
|
|
|
}
|
|
|
|
|
|
2019-10-31 00:06:10 -05:00
|
|
|
// DependencyInfo represents high level information about an issue which is a dependency of another issue.
|
|
|
|
|
type DependencyInfo struct {
|
2021-12-10 09:27:50 +08:00
|
|
|
Issue `xorm:"extends"`
|
|
|
|
|
repo_model.Repository `xorm:"extends"`
|
2019-10-31 00:06:10 -05:00
|
|
|
}
|
|
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
// GetParticipantIDsByIssue returns all userIDs who are participated in comments of an issue and issue author
|
|
|
|
|
func (issue *Issue) GetParticipantIDsByIssue(ctx context.Context) ([]int64, error) {
|
2020-02-28 09:16:41 +01:00
|
|
|
if issue == nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
userIDs := make([]int64, 0, 5)
|
2022-05-20 22:08:52 +08:00
|
|
|
if err := db.GetEngine(ctx).Table("comment").Cols("poster_id").
|
2020-02-28 09:16:41 +01:00
|
|
|
Where("`comment`.issue_id = ?", issue.ID).
|
|
|
|
|
And("`comment`.type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview).
|
|
|
|
|
And("`user`.is_active = ?", true).
|
|
|
|
|
And("`user`.prohibit_login = ?", false).
|
|
|
|
|
Join("INNER", "`user`", "`user`.id = `comment`.poster_id").
|
|
|
|
|
Distinct("poster_id").
|
|
|
|
|
Find(&userIDs); err != nil {
|
2022-10-24 21:29:17 +02:00
|
|
|
return nil, fmt.Errorf("get poster IDs: %w", err)
|
2020-02-28 09:16:41 +01:00
|
|
|
}
|
2023-01-11 13:31:16 +08:00
|
|
|
if !util.SliceContains(userIDs, issue.PosterID) {
|
2020-02-28 09:16:41 +01:00
|
|
|
return append(userIDs, issue.PosterID), nil
|
|
|
|
|
}
|
|
|
|
|
return userIDs, nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-20 22:08:52 +08:00
|
|
|
// BlockedByDependencies finds all Dependencies an issue is blocked by
|
2023-03-28 19:23:25 +02:00
|
|
|
func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptions) (issueDeps []*DependencyInfo, err error) {
|
|
|
|
|
sess := db.GetEngine(ctx).
|
2019-10-31 00:06:10 -05:00
|
|
|
Table("issue").
|
|
|
|
|
Join("INNER", "repository", "repository.id = issue.repo_id").
|
|
|
|
|
Join("INNER", "issue_dependency", "issue_dependency.dependency_id = issue.id").
|
2018-07-17 23:23:58 +02:00
|
|
|
Where("issue_id = ?", issue.ID).
|
2021-03-15 02:52:12 +08:00
|
|
|
// sort by repo id then created date, with the issues of the same repo at the beginning of the list
|
2023-03-28 19:23:25 +02:00
|
|
|
OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID)
|
|
|
|
|
if opts.Page != 0 {
|
|
|
|
|
sess = db.SetSessionPagination(sess, &opts)
|
|
|
|
|
}
|
|
|
|
|
err = sess.Find(&issueDeps)
|
2021-11-18 08:18:12 +00:00
|
|
|
|
|
|
|
|
for _, depInfo := range issueDeps {
|
|
|
|
|
depInfo.Issue.Repo = &depInfo.Repository
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return issueDeps, err
|
2018-07-17 23:23:58 +02:00
|
|
|
}
|
|
|
|
|
|
2022-05-20 22:08:52 +08:00
|
|
|
// BlockingDependencies returns all blocking dependencies, aka all other issues a given issue blocks
|
|
|
|
|
func (issue *Issue) BlockingDependencies(ctx context.Context) (issueDeps []*DependencyInfo, err error) {
|
|
|
|
|
err = db.GetEngine(ctx).
|
2019-10-31 00:06:10 -05:00
|
|
|
Table("issue").
|
|
|
|
|
Join("INNER", "repository", "repository.id = issue.repo_id").
|
|
|
|
|
Join("INNER", "issue_dependency", "issue_dependency.issue_id = issue.id").
|
2018-07-17 23:23:58 +02:00
|
|
|
Where("dependency_id = ?", issue.ID).
|
2021-03-15 02:52:12 +08:00
|
|
|
// sort by repo id then created date, with the issues of the same repo at the beginning of the list
|
2022-06-05 03:18:50 +08:00
|
|
|
OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID).
|
2018-07-17 23:23:58 +02:00
|
|
|
Find(&issueDeps)
|
2021-11-18 08:18:12 +00:00
|
|
|
|
|
|
|
|
for _, depInfo := range issueDeps {
|
|
|
|
|
depInfo.Issue.Repo = &depInfo.Repository
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return issueDeps, err
|
2018-07-17 23:23:58 +02:00
|
|
|
}
|
|
|
|
|
|
2022-06-13 17:37:59 +08:00
|
|
|
func migratedIssueCond(tp api.GitServiceType) builder.Cond {
|
|
|
|
|
return builder.In("issue_id",
|
|
|
|
|
builder.Select("issue.id").
|
|
|
|
|
From("issue").
|
|
|
|
|
InnerJoin("repository", "issue.repo_id = repository.id").
|
|
|
|
|
Where(builder.Eq{
|
|
|
|
|
"repository.original_service_type": tp,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-01 19:20:28 +01:00
|
|
|
// RemapExternalUser ExternalUserRemappable interface
|
|
|
|
|
func (issue *Issue) RemapExternalUser(externalName string, externalID, userID int64) error {
|
|
|
|
|
issue.OriginalAuthor = externalName
|
|
|
|
|
issue.OriginalAuthorID = externalID
|
|
|
|
|
issue.PosterID = userID
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetUserID ExternalUserRemappable interface
|
|
|
|
|
func (issue *Issue) GetUserID() int64 { return issue.PosterID }
|
|
|
|
|
|
|
|
|
|
// GetExternalName ExternalUserRemappable interface
|
|
|
|
|
func (issue *Issue) GetExternalName() string { return issue.OriginalAuthor }
|
|
|
|
|
|
|
|
|
|
// GetExternalID ExternalUserRemappable interface
|
|
|
|
|
func (issue *Issue) GetExternalID() int64 { return issue.OriginalAuthorID }
|
2022-06-13 17:37:59 +08:00
|
|
|
|
2023-02-16 01:29:13 +08:00
|
|
|
// HasOriginalAuthor returns if an issue was migrated and has an original author.
|
|
|
|
|
func (issue *Issue) HasOriginalAuthor() bool {
|
|
|
|
|
return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0
|
|
|
|
|
}
|