935 lines · 26.5 KB · Go
RawDownload
// Copyright 2014 The Gogs Authors. All rights reserved.// Copyright 2020 The Gitea Authors. All rights reserved.// SPDX-License-Identifier: MIT package issues import (	"context"	"fmt"	"html/template"	"regexp"	"slices" 	"forgejo.org/models/db"	project_model "forgejo.org/models/project"	repo_model "forgejo.org/models/repo"	user_model "forgejo.org/models/user"	"forgejo.org/modules/container"	"forgejo.org/modules/log"	"forgejo.org/modules/setting"	api "forgejo.org/modules/structs"	"forgejo.org/modules/timeutil"	"forgejo.org/modules/util" 	"xorm.io/builder") // 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)} func (err ErrIssueNotExist) Unwrap() error {	return util.ErrNotExist} // 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)} // ErrIssueWasClosed is used when close a closed issuetype 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)} var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already changed") // Issue represents an issue or pull request of repository.type Issue struct {	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:"-"`	OriginalAuthor    string	OriginalAuthorID  int64                  `xorm:"index"`	Title             string                 `xorm:"name"`	Content           string                 `xorm:"LONGTEXT"`	RenderedContent   template.HTML          `xorm:"-"`	ContentVersion    int                    `xorm:"NOT NULL DEFAULT 0"`	Labels            []*Label               `xorm:"-"`	isLabelsLoaded    bool                   `xorm:"-"`	MilestoneID       int64                  `xorm:"INDEX"`	Milestone         *Milestone             `xorm:"-"`	isMilestoneLoaded bool                   `xorm:"-"`	Project           *project_model.Project `xorm:"-"`	Priority          int	AssigneeID        int64            `xorm:"-"`	Assignee          *user_model.User `xorm:"-"`	isAssigneeLoaded  bool             `xorm:"-"`	IsClosed          bool             `xorm:"INDEX"`	IsRead            bool             `xorm:"-"`	IsPull            bool             `xorm:"INDEX"` // Indicates whether is a pull request or not.	PullRequest       *PullRequest     `xorm:"-"`	NumComments       int	Ref               string	PinOrder          int `xorm:"DEFAULT 0"` 	DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"` 	Created timeutil.TimeStampNano 	CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`	UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`	ClosedUnix  timeutil.TimeStamp `xorm:"INDEX"`	NoAutoTime  bool               `xorm:"-"` 	Attachments         []*repo_model.Attachment `xorm:"-"`	isAttachmentsLoaded bool                     `xorm:"-"`	Comments            CommentList              `xorm:"-"`	Reactions           ReactionList             `xorm:"-"`	TotalTrackedTime    int64                    `xorm:"-"`	Assignees           []*user_model.User       `xorm:"-"` 	// IsLocked limits commenting abilities to users on an issue	// with write access	IsLocked bool `xorm:"NOT NULL DEFAULT false"` 	// For view issue page.	ShowRole RoleDescriptor `xorm:"-"`} var (	issueTasksPat     = regexp.MustCompile(`(^|\n)\s*[-*]\s*\[[\sxX]\]`)	issueTasksDonePat = regexp.MustCompile(`(^|\n)\s*[-*]\s*\[[xX]\]`)) // IssueIndex represents the issue index tabletype IssueIndex db.ResourceIndex func init() {	db.RegisterModel(new(Issue))	db.RegisterModel(new(IssueIndex))} // LoadTotalTimes load total tracked timefunc (issue *Issue) LoadTotalTimes(ctx context.Context) (err error) {	opts := FindTrackedTimesOptions{IssueID: issue.ID}	issue.TotalTrackedTime, err = opts.toSession(db.GetEngine(ctx)).SumInt(&TrackedTime{}, "time")	if err != nil {		return err	}	return nil} // IsOverdue checks if the issue is overduefunc (issue *Issue) IsOverdue() bool {	if issue.IsClosed {		return issue.ClosedUnix >= issue.DeadlineUnix	}	return timeutil.TimeStampNow() >= issue.DeadlineUnix} // LoadRepo loads issue's repositoryfunc (issue *Issue) LoadRepo(ctx context.Context) (err error) {	if issue.Repo == nil && issue.RepoID != 0 {		issue.Repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID)		if err != nil {			return fmt.Errorf("getRepositoryByID [%d]: %w", issue.RepoID, err)		}	}	return nil} func (issue *Issue) LoadAttachments(ctx context.Context) (err error) {	if issue.isAttachmentsLoaded || issue.Attachments != nil {		return nil	} 	issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)	if err != nil {		return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)	}	issue.isAttachmentsLoaded = true	return nil} // IsTimetrackerEnabled returns true if the repo enables timetrackingfunc (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {	if err := issue.LoadRepo(ctx); err != nil {		log.Error(fmt.Sprintf("loadRepo: %v", err))		return false	}	return issue.Repo.IsTimetrackerEnabled(ctx)} // LoadPoster loads posterfunc (issue *Issue) LoadPoster(ctx context.Context) (err error) {	if issue.Poster == nil && issue.PosterID != 0 {		issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID)		if err != nil {			issue.PosterID = user_model.GhostUserID			issue.Poster = user_model.NewGhostUser()			if !user_model.IsErrUserNotExist(err) {				return fmt.Errorf("getUserByID.(poster) [%d]: %w", issue.PosterID, err)			}			return nil		}	}	return err} // LoadPullRequest loads pull request infofunc (issue *Issue) LoadPullRequest(ctx context.Context) (err error) {	if issue.IsPull {		if issue.PullRequest == nil && issue.ID != 0 {			issue.PullRequest, err = GetPullRequestByIssueID(ctx, issue.ID)			if err != nil {				if IsErrPullRequestNotExist(err) {					return err				}				return fmt.Errorf("getPullRequestByIssueID [%d]: %w", issue.ID, err)			}		}		if issue.PullRequest != nil {			issue.PullRequest.Issue = issue		}	}	return nil} func (issue *Issue) loadComments(ctx context.Context) (err error) {	return issue.loadCommentsByType(ctx, CommentTypeUndefined)} // LoadDiscussComments loads discuss commentsfunc (issue *Issue) LoadDiscussComments(ctx context.Context) error {	return issue.loadCommentsByType(ctx, CommentTypeComment)} func (issue *Issue) loadCommentsByType(ctx context.Context, tp CommentType) (err error) {	if issue.Comments != nil {		return nil	}	issue.Comments, err = FindComments(ctx, &FindCommentsOptions{		IssueID: issue.ID,		Type:    tp,	})	for _, comment := range issue.Comments {		comment.Issue = issue	}	return err} func (issue *Issue) loadReactions(ctx context.Context) (err error) {	if issue.Reactions != nil {		return nil	}	reactions, _, err := FindReactions(ctx, FindReactionsOptions{		IssueID: issue.ID,	})	if err != nil {		return err	}	if err = issue.LoadRepo(ctx); err != nil {		return err	}	// Load reaction user data	if _, err := reactions.LoadUsers(ctx, issue.Repo); err != nil {		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} // LoadMilestone load milestone of this issue.func (issue *Issue) LoadMilestone(ctx context.Context) (err error) {	if !issue.isMilestoneLoaded && (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {		issue.Milestone, err = GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID)		if err != nil && !IsErrMilestoneNotExist(err) {			return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %w", issue.RepoID, issue.MilestoneID, err)		}		issue.isMilestoneLoaded = true	}	return nil} // LoadAttributes loads the attribute of this issue.func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {	if err = issue.LoadRepo(ctx); err != nil {		return err	} 	if err = issue.LoadPoster(ctx); err != nil {		return err	} 	if err = issue.LoadLabels(ctx); err != nil {		return err	} 	if err = issue.LoadMilestone(ctx); err != nil {		return err	} 	if err = issue.LoadProject(ctx); err != nil {		return err	} 	if err = issue.LoadAssignees(ctx); err != nil {		return err	} 	if err = issue.LoadPullRequest(ctx); err != nil && !IsErrPullRequestNotExist(err) {		// It is possible pull request is not yet created.		return err	} 	if err = issue.LoadAttachments(ctx); err != nil {		return err	} 	if err = issue.loadComments(ctx); err != nil {		return err	} 	if err = issue.Comments.LoadAttributes(ctx); err != nil {		return err	}	if issue.IsTimetrackerEnabled(ctx) {		if err = issue.LoadTotalTimes(ctx); err != nil {			return err		}	} 	return issue.loadReactions(ctx)} func (issue *Issue) ResetAttributesLoaded() {	issue.isLabelsLoaded = false	issue.isMilestoneLoaded = false	issue.isAttachmentsLoaded = false	issue.isAssigneeLoaded = false} // GetIsRead load the `IsRead` field of the issuefunc (issue *Issue) GetIsRead(ctx context.Context, userID int64) error {	issueUser := &IssueUser{IssueID: issue.ID, UID: userID}	if has, err := db.GetEngine(ctx).Get(issueUser); err != nil {		return err	} else if !has {		issue.IsRead = false		return nil	}	issue.IsRead = issueUser.IsRead	return nil} // APIURL returns the absolute APIURL to this issue.func (issue *Issue) APIURL(ctx context.Context) string {	if issue.Repo == nil {		err := issue.LoadRepo(ctx)		if err != nil {			log.Error("Issue[%d].APIURL(): %v", issue.ID, err)			return ""		}	}	return fmt.Sprintf("%s/issues/%d", issue.Repo.APIURL(), issue.Index)} // HTMLURL returns the absolute URL to this issue.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)} // SummaryCardURL returns the absolute URL to an image providing a summary of the issuefunc (issue *Issue) SummaryCardURL() string {	return fmt.Sprintf("%s/summary-card", issue.HTMLURL())} // Link returns the issue's relative URL.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)} // DiffURL returns the absolute URL to this difffunc (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 patchfunc (issue *Issue) PatchURL() string {	if issue.IsPull {		return fmt.Sprintf("%s/pulls/%d.patch", issue.Repo.HTMLURL(), issue.Index)	}	return ""} // State returns string representation of issue status.func (issue *Issue) State() api.StateType {	if issue.IsClosed {		return api.StateClosed	}	return api.StateOpen} // HashTag returns unique hash tag for issue.func (issue *Issue) HashTag() string {	return fmt.Sprintf("issue-%d", issue.ID)} // IsPoster returns true if given user by ID is the poster.func (issue *Issue) IsPoster(uid int64) bool {	return issue.OriginalAuthorID == 0 && issue.PosterID == uid} // GetTasks returns the amount of tasks in the issues contentfunc (issue *Issue) GetTasks() int {	return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))} // GetTasksDone returns the amount of completed tasks in the issues contentfunc (issue *Issue) GetTasksDone() int {	return len(issueTasksDonePat.FindAllStringIndex(issue.Content, -1))} // 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	}	return issue.CreatedUnix} // 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"	}	return "repo.issues.opened_by"} // GetLastComment return last comment for the current issue.func (issue *Issue) GetLastComment(ctx context.Context) (*Comment, error) {	var c Comment	exist, err := db.GetEngine(ctx).Where("type = ?", CommentTypeComment).		And("issue_id = ?", issue.ID).Desc("created_unix").Get(&c)	if err != nil {		return nil, err	}	if !exist {		return nil, nil	}	return &c, nil} // 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"		}

Lines 1–500 of 935.