// 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. Load more
// 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 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) } 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 table type IssueIndex db.ResourceIndex func init() { db.RegisterModel(new(Issue)) db.RegisterModel(new(IssueIndex)) } // LoadTotalTimes load total tracked time func (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 overdue func (issue *Issue) IsOverdue() bool { if issue.IsClosed { return issue.ClosedUnix >= issue.DeadlineUnix } return timeutil.TimeStampNow() >= issue.DeadlineUnix } // LoadRepo loads issue's repository func (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 timetracking func (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 poster func (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 info func (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 comments func (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 issue func (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 issue func (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 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 "" } // 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 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)) } // 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" }