// Copyright 2018 The Gitea Authors.// Copyright 2016 The Gogs Authors.// All rights reserved.// SPDX-License-Identifier: MIT package issues import ( "context" "fmt" "html/template" "strconv" "unicode/utf8" "forgejo.org/models/db" git_model "forgejo.org/models/git" "forgejo.org/models/organization" 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/gitrepo" "forgejo.org/modules/json" "forgejo.org/modules/log" "forgejo.org/modules/optional" "forgejo.org/modules/references" "forgejo.org/modules/structs" "forgejo.org/modules/timeutil" "forgejo.org/modules/translation" "forgejo.org/modules/util" "xorm.io/builder") // ErrCommentNotExist represents a "CommentNotExist" kind of error.type ErrCommentNotExist struct { ID int64 IssueID int64} // IsErrCommentNotExist checks if an error is a ErrCommentNotExist.func IsErrCommentNotExist(err error) bool { _, ok := err.(ErrCommentNotExist) return ok} func (err ErrCommentNotExist) Error() string { return fmt.Sprintf("comment does not exist [id: %d, issue_id: %d]", err.ID, err.IssueID)} func (err ErrCommentNotExist) Unwrap() error { return util.ErrNotExist} var ErrCommentAlreadyChanged = util.NewInvalidArgumentErrorf("the comment is already changed") // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.type CommentType int // CommentTypeUndefined is used to search for comments of any typeconst CommentTypeUndefined CommentType = -1 const ( CommentTypeComment CommentType = iota // 0 Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0) CommentTypeReopen // 1 CommentTypeClose // 2 CommentTypeIssueRef // 3 References. CommentTypeCommitRef // 4 Reference from a commit (not part of a pull request) CommentTypeCommentRef // 5 Reference from a comment CommentTypePullRef // 6 Reference from a pull request CommentTypeLabel // 7 Labels changed CommentTypeMilestone // 8 Milestone changed CommentTypeAssignees // 9 Assignees changed CommentTypeChangeTitle // 10 Change Title CommentTypeDeleteBranch // 11 Delete Branch CommentTypeStartTracking // 12 Start a stopwatch for time tracking CommentTypeStopTracking // 13 Stop a stopwatch for time tracking CommentTypeAddTimeManual // 14 Add time manual for time tracking CommentTypeCancelTracking // 15 Cancel a stopwatch for time tracking CommentTypeAddedDeadline // 16 Added a due date CommentTypeModifiedDeadline // 17 Modified the due date CommentTypeRemovedDeadline // 18 Removed a due date CommentTypeAddDependency // 19 Dependency added CommentTypeRemoveDependency // 20 Dependency removed CommentTypeCode // 21 Comment a line of code CommentTypeReview // 22 Reviews a pull request by giving general feedback CommentTypeLock // 23 Lock an issue, giving only collaborators access CommentTypeUnlock // 24 Unlocks a previously locked issue CommentTypeChangeTargetBranch // 25 Change pull request's target branch CommentTypeDeleteTimeManual // 26 Delete time manual for time tracking CommentTypeReviewRequest // 27 add or remove Request from one CommentTypeMergePull // 28 merge pull request CommentTypePullRequestPush // 29 push to PR head branch CommentTypeProject // 30 Project changed CommentTypeProjectColumn // 31 Project column changed CommentTypeDismissReview // 32 Dismiss Review CommentTypeChangeIssueRef // 33 Change issue ref CommentTypePRScheduledToAutoMerge // 34 pr was scheduled to auto merge when checks succeed CommentTypePRUnScheduledToAutoMerge // 35 pr was un scheduled to auto merge when checks succeed CommentTypePin // 36 pin Issue CommentTypeUnpin // 37 unpin Issue CommentTypeAggregator // 38 Aggregator of comments) var commentStrings = []string{ "comment", "reopen", "close", "issue_ref", "commit_ref", "comment_ref", "pull_ref", "label", "milestone", "assignees", "change_title", "delete_branch", "start_tracking", "stop_tracking", "add_time_manual", "cancel_tracking", "added_deadline", "modified_deadline", "removed_deadline", "add_dependency", "remove_dependency", "code", "review", "lock", "unlock", "change_target_branch", "delete_time_manual", "review_request", "merge_pull", "pull_push", "project", "project_board", // FIXME: the name should be project_column "dismiss_review", "change_issue_ref", "pull_scheduled_merge", "pull_cancel_scheduled_merge", "pin", "unpin", "action_aggregator",} func (t CommentType) String() string { return commentStrings[t]} func AsCommentType(typeName string) CommentType { for index, name := range commentStrings { if typeName == name { return CommentType(index) } } return CommentTypeUndefined} func (t CommentType) HasContentSupport() bool { switch t { case CommentTypeComment, CommentTypeCode, CommentTypeReview, CommentTypeDismissReview: return true } return false} func (t CommentType) HasAttachmentSupport() bool { switch t { case CommentTypeComment, CommentTypeCode, CommentTypeReview: return true } return false} func (t CommentType) HasMailReplySupport() bool { switch t { case CommentTypeComment, CommentTypeCode, CommentTypeReview, CommentTypeDismissReview, CommentTypeReopen, CommentTypeClose, CommentTypeMergePull, CommentTypeAssignees: return true } return false} func (t CommentType) CountedAsConversation() bool { for _, ct := range ConversationCountedCommentType() { if t == ct { return true } } return false} // ConversationCountedCommentType returns the comment types that are counted as a conversationfunc ConversationCountedCommentType() []CommentType { return []CommentType{CommentTypeComment, CommentTypeReview}} // RoleInRepo presents the user's participation in the repotype RoleInRepo string // RoleDescriptor defines comment "role" tagstype RoleDescriptor struct { IsPoster bool RoleInRepo RoleInRepo} // Enumerate all the role tags.const ( RoleRepoOwner RoleInRepo = "owner" RoleRepoMember RoleInRepo = "member" RoleRepoCollaborator RoleInRepo = "collaborator" RoleRepoFirstTimeContributor RoleInRepo = "first_time_contributor" RoleRepoContributor RoleInRepo = "contributor") // LocaleString returns the locale string name of the rolefunc (r RoleInRepo) LocaleString(lang translation.Locale) string { return lang.TrString("repo.issues.role." + string(r))} // LocaleHelper returns the locale tooltip of the rolefunc (r RoleInRepo) LocaleHelper(lang translation.Locale) string { return lang.TrString("repo.issues.role." + string(r) + "_helper")} // Comment represents a comment in commit and issue page.type Comment struct { ID int64 `xorm:"pk autoincr"` Type CommentType `xorm:"INDEX"` PosterID int64 `xorm:"INDEX"` Poster *user_model.User `xorm:"-"` OriginalAuthor string OriginalAuthorID int64 IssueID int64 `xorm:"INDEX"` Issue *Issue `xorm:"-"` LabelID int64 Label *Label `xorm:"-"` Aggregator *ActionAggregator `xorm:"-"` AddedLabels []*Label `xorm:"-"` RemovedLabels []*Label `xorm:"-"` AddedRequestReview []RequestReviewTarget `xorm:"-"` RemovedRequestReview []RequestReviewTarget `xorm:"-"` OldProjectID int64 ProjectID int64 OldProject *project_model.Project `xorm:"-"` Project *project_model.Project `xorm:"-"` OldMilestoneID int64 MilestoneID int64 OldMilestone *Milestone `xorm:"-"` Milestone *Milestone `xorm:"-"` TimeID int64 Time *TrackedTime `xorm:"-"` AssigneeID int64 RemovedAssignee bool Assignee *user_model.User `xorm:"-"` AssigneeTeamID int64 `xorm:"NOT NULL DEFAULT 0"` AssigneeTeam *organization.Team `xorm:"-"` ResolveDoerID int64 ResolveDoer *user_model.User `xorm:"-"` OldTitle string NewTitle string OldRef string NewRef string DependentIssueID int64 `xorm:"index"` // This is used by issue_service.deleteIssue DependentIssue *Issue `xorm:"-"` CommitID int64 Line int64 // - previous line / + proposed line TreePath string Content string `xorm:"LONGTEXT"` ContentVersion int `xorm:"NOT NULL DEFAULT 0"` RenderedContent template.HTML `xorm:"-"` // Path represents the 4 lines of code cemented by this comment Patch string `xorm:"-"` PatchQuoted string `xorm:"LONGTEXT patch"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` // Reference issue in commit message CommitSHA string `xorm:"VARCHAR(64)"` Attachments []*repo_model.Attachment `xorm:"-"` Reactions ReactionList `xorm:"-"` // For view issue page. ShowRole RoleDescriptor `xorm:"-"` Review *Review `xorm:"-"` ReviewID int64 `xorm:"index"` Invalidated bool // Reference an issue or pull from another comment, issue or PR // All information is about the origin of the reference RefRepoID int64 `xorm:"index"` // Repo where the referencing RefIssueID int64 `xorm:"index"` RefCommentID int64 `xorm:"index"` // 0 if origin is Issue title or content (or PR's) RefAction references.XRefAction `xorm:"SMALLINT"` // What happens if RefIssueID resolves RefIsPull bool RefRepo *repo_model.Repository `xorm:"-"` RefIssue *Issue `xorm:"-"` RefComment *Comment `xorm:"-"` Commits []*git_model.SignCommitWithStatuses `xorm:"-"` OldCommit string `xorm:"-"` NewCommit string `xorm:"-"` CommitsNum int64 `xorm:"-"` IsForcePush bool `xorm:"-"`} func init() { db.RegisterModel(new(Comment))} // PushActionContent is content of push pull commenttype PushActionContent struct { IsForcePush bool `json:"is_force_push"` CommitIDs []string `json:"commit_ids"`} // LoadIssue loads the issue reference for the commentfunc (c *Comment) LoadIssue(ctx context.Context) (err error) { if c.Issue != nil { return nil } c.Issue, err = GetIssueByID(ctx, c.IssueID) return err} // BeforeInsert will be invoked by XORM before inserting a recordfunc (c *Comment) BeforeInsert() { c.PatchQuoted = c.Patch if !utf8.ValidString(c.Patch) { c.PatchQuoted = strconv.Quote(c.Patch) }} // BeforeUpdate will be invoked by XORM before updating a recordfunc (c *Comment) BeforeUpdate() { c.PatchQuoted = c.Patch if !utf8.ValidString(c.Patch) { c.PatchQuoted = strconv.Quote(c.Patch) }} // AfterLoad is invoked from XORM after setting the values of all fields of this object.func (c *Comment) AfterLoad() { c.Patch = c.PatchQuoted if len(c.PatchQuoted) > 0 && c.PatchQuoted[0] == '"' { unquoted, err := strconv.Unquote(c.PatchQuoted) if err == nil { c.Patch = unquoted } }} // LoadPoster loads comment posterfunc (c *Comment) LoadPoster(ctx context.Context) (err error) { if c.Poster != nil { return nil } c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID) if err != nil { if user_model.IsErrUserNotExist(err) { c.PosterID = user_model.GhostUserID c.Poster = user_model.NewGhostUser() } else { log.Error("getUserByID[%d]: %v", c.ID, err) } } return err} // AfterDelete is invoked from XORM after the object is deleted.func (c *Comment) AfterDelete(ctx context.Context) { if c.ID <= 0 { return } _, err := repo_model.DeleteAttachmentsByComment(ctx, c.ID, true) if err != nil { log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err) }} // HTMLURL formats a URL-string to the issue-commentfunc (c *Comment) HTMLURL(ctx context.Context) string { err := c.LoadIssue(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("LoadIssue(%d): %v", c.IssueID, err) return "" } err = c.Issue.LoadRepo(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" } return c.Issue.HTMLURL() + c.hashLink(ctx)} // Link formats a relative URL-string to the issue-commentfunc (c *Comment) Link(ctx context.Context) string { err := c.LoadIssue(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("LoadIssue(%d): %v", c.IssueID, err) return "" } err = c.Issue.LoadRepo(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" } return c.Issue.Link() + c.hashLink(ctx)} func (c *Comment) hashLink(ctx context.Context) string { if c.Type == CommentTypeCode { if c.ReviewID == 0 { return "/files#" + c.HashTag() } if c.Review == nil { if err := c.LoadReview(ctx); err != nil { log.Warn("LoadReview(%d): %v", c.ReviewID, err) return "/files#" + c.HashTag() } } if c.Review.Type <= ReviewTypePending { return "/files#" + c.HashTag() } } return "#" + c.HashTag()} // APIURL formats a API-string to the issue-commentfunc (c *Comment) APIURL(ctx context.Context) string { err := c.LoadIssue(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("LoadIssue(%d): %v", c.IssueID, err) return "" } err = c.Issue.LoadRepo(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" } return fmt.Sprintf("%s/issues/comments/%d", c.Issue.Repo.APIURL(), c.ID)} // IssueURL formats a URL-string to the issuefunc (c *Comment) IssueURL(ctx context.Context) string { err := c.LoadIssue(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("LoadIssue(%d): %v", c.IssueID, err) return "" } if c.Issue.IsPull { return "" } err = c.Issue.LoadRepo(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" } return c.Issue.HTMLURL()} // PRURL formats a URL-string to the pull-requestfunc (c *Comment) PRURL(ctx context.Context) string { err := c.LoadIssue(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("LoadIssue(%d): %v", c.IssueID, err) return "" } err = c.Issue.LoadRepo(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return ""Lines 1–500 of 1367. Load more
// Copyright 2018 The Gitea Authors. // Copyright 2016 The Gogs Authors. // All rights reserved. // SPDX-License-Identifier: MIT package issues import ( "context" "fmt" "html/template" "strconv" "unicode/utf8" "forgejo.org/models/db" git_model "forgejo.org/models/git" "forgejo.org/models/organization" 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/gitrepo" "forgejo.org/modules/json" "forgejo.org/modules/log" "forgejo.org/modules/optional" "forgejo.org/modules/references" "forgejo.org/modules/structs" "forgejo.org/modules/timeutil" "forgejo.org/modules/translation" "forgejo.org/modules/util" "xorm.io/builder" ) // ErrCommentNotExist represents a "CommentNotExist" kind of error. type ErrCommentNotExist struct { ID int64 IssueID int64 } // IsErrCommentNotExist checks if an error is a ErrCommentNotExist. func IsErrCommentNotExist(err error) bool { _, ok := err.(ErrCommentNotExist) return ok } func (err ErrCommentNotExist) Error() string { return fmt.Sprintf("comment does not exist [id: %d, issue_id: %d]", err.ID, err.IssueID) } func (err ErrCommentNotExist) Unwrap() error { return util.ErrNotExist } var ErrCommentAlreadyChanged = util.NewInvalidArgumentErrorf("the comment is already changed") // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference. type CommentType int // CommentTypeUndefined is used to search for comments of any type const CommentTypeUndefined CommentType = -1 const ( CommentTypeComment CommentType = iota // 0 Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0) CommentTypeReopen // 1 CommentTypeClose // 2 CommentTypeIssueRef // 3 References. CommentTypeCommitRef // 4 Reference from a commit (not part of a pull request) CommentTypeCommentRef // 5 Reference from a comment CommentTypePullRef // 6 Reference from a pull request CommentTypeLabel // 7 Labels changed CommentTypeMilestone // 8 Milestone changed CommentTypeAssignees // 9 Assignees changed CommentTypeChangeTitle // 10 Change Title CommentTypeDeleteBranch // 11 Delete Branch CommentTypeStartTracking // 12 Start a stopwatch for time tracking CommentTypeStopTracking // 13 Stop a stopwatch for time tracking CommentTypeAddTimeManual // 14 Add time manual for time tracking CommentTypeCancelTracking // 15 Cancel a stopwatch for time tracking CommentTypeAddedDeadline // 16 Added a due date CommentTypeModifiedDeadline // 17 Modified the due date CommentTypeRemovedDeadline // 18 Removed a due date CommentTypeAddDependency // 19 Dependency added CommentTypeRemoveDependency // 20 Dependency removed CommentTypeCode // 21 Comment a line of code CommentTypeReview // 22 Reviews a pull request by giving general feedback CommentTypeLock // 23 Lock an issue, giving only collaborators access CommentTypeUnlock // 24 Unlocks a previously locked issue CommentTypeChangeTargetBranch // 25 Change pull request's target branch CommentTypeDeleteTimeManual // 26 Delete time manual for time tracking CommentTypeReviewRequest // 27 add or remove Request from one CommentTypeMergePull // 28 merge pull request CommentTypePullRequestPush // 29 push to PR head branch CommentTypeProject // 30 Project changed CommentTypeProjectColumn // 31 Project column changed CommentTypeDismissReview // 32 Dismiss Review CommentTypeChangeIssueRef // 33 Change issue ref CommentTypePRScheduledToAutoMerge // 34 pr was scheduled to auto merge when checks succeed CommentTypePRUnScheduledToAutoMerge // 35 pr was un scheduled to auto merge when checks succeed CommentTypePin // 36 pin Issue CommentTypeUnpin // 37 unpin Issue CommentTypeAggregator // 38 Aggregator of comments ) var commentStrings = []string{ "comment", "reopen", "close", "issue_ref", "commit_ref", "comment_ref", "pull_ref", "label", "milestone", "assignees", "change_title", "delete_branch", "start_tracking", "stop_tracking", "add_time_manual", "cancel_tracking", "added_deadline", "modified_deadline", "removed_deadline", "add_dependency", "remove_dependency", "code", "review", "lock", "unlock", "change_target_branch", "delete_time_manual", "review_request", "merge_pull", "pull_push", "project", "project_board", // FIXME: the name should be project_column "dismiss_review", "change_issue_ref", "pull_scheduled_merge", "pull_cancel_scheduled_merge", "pin", "unpin", "action_aggregator", } func (t CommentType) String() string { return commentStrings[t] } func AsCommentType(typeName string) CommentType { for index, name := range commentStrings { if typeName == name { return CommentType(index) } } return CommentTypeUndefined } func (t CommentType) HasContentSupport() bool { switch t { case CommentTypeComment, CommentTypeCode, CommentTypeReview, CommentTypeDismissReview: return true } return false } func (t CommentType) HasAttachmentSupport() bool { switch t { case CommentTypeComment, CommentTypeCode, CommentTypeReview: return true } return false } func (t CommentType) HasMailReplySupport() bool { switch t { case CommentTypeComment, CommentTypeCode, CommentTypeReview, CommentTypeDismissReview, CommentTypeReopen, CommentTypeClose, CommentTypeMergePull, CommentTypeAssignees: return true } return false } func (t CommentType) CountedAsConversation() bool { for _, ct := range ConversationCountedCommentType() { if t == ct { return true } } return false } // ConversationCountedCommentType returns the comment types that are counted as a conversation func ConversationCountedCommentType() []CommentType { return []CommentType{CommentTypeComment, CommentTypeReview} } // RoleInRepo presents the user's participation in the repo type RoleInRepo string // RoleDescriptor defines comment "role" tags type RoleDescriptor struct { IsPoster bool RoleInRepo RoleInRepo } // Enumerate all the role tags. const ( RoleRepoOwner RoleInRepo = "owner" RoleRepoMember RoleInRepo = "member" RoleRepoCollaborator RoleInRepo = "collaborator" RoleRepoFirstTimeContributor RoleInRepo = "first_time_contributor" RoleRepoContributor RoleInRepo = "contributor" ) // LocaleString returns the locale string name of the role func (r RoleInRepo) LocaleString(lang translation.Locale) string { return lang.TrString("repo.issues.role." + string(r)) } // LocaleHelper returns the locale tooltip of the role func (r RoleInRepo) LocaleHelper(lang translation.Locale) string { return lang.TrString("repo.issues.role." + string(r) + "_helper") } // Comment represents a comment in commit and issue page. type Comment struct { ID int64 `xorm:"pk autoincr"` Type CommentType `xorm:"INDEX"` PosterID int64 `xorm:"INDEX"` Poster *user_model.User `xorm:"-"` OriginalAuthor string OriginalAuthorID int64 IssueID int64 `xorm:"INDEX"` Issue *Issue `xorm:"-"` LabelID int64 Label *Label `xorm:"-"` Aggregator *ActionAggregator `xorm:"-"` AddedLabels []*Label `xorm:"-"` RemovedLabels []*Label `xorm:"-"` AddedRequestReview []RequestReviewTarget `xorm:"-"` RemovedRequestReview []RequestReviewTarget `xorm:"-"` OldProjectID int64 ProjectID int64 OldProject *project_model.Project `xorm:"-"` Project *project_model.Project `xorm:"-"` OldMilestoneID int64 MilestoneID int64 OldMilestone *Milestone `xorm:"-"` Milestone *Milestone `xorm:"-"` TimeID int64 Time *TrackedTime `xorm:"-"` AssigneeID int64 RemovedAssignee bool Assignee *user_model.User `xorm:"-"` AssigneeTeamID int64 `xorm:"NOT NULL DEFAULT 0"` AssigneeTeam *organization.Team `xorm:"-"` ResolveDoerID int64 ResolveDoer *user_model.User `xorm:"-"` OldTitle string NewTitle string OldRef string NewRef string DependentIssueID int64 `xorm:"index"` // This is used by issue_service.deleteIssue DependentIssue *Issue `xorm:"-"` CommitID int64 Line int64 // - previous line / + proposed line TreePath string Content string `xorm:"LONGTEXT"` ContentVersion int `xorm:"NOT NULL DEFAULT 0"` RenderedContent template.HTML `xorm:"-"` // Path represents the 4 lines of code cemented by this comment Patch string `xorm:"-"` PatchQuoted string `xorm:"LONGTEXT patch"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` // Reference issue in commit message CommitSHA string `xorm:"VARCHAR(64)"` Attachments []*repo_model.Attachment `xorm:"-"` Reactions ReactionList `xorm:"-"` // For view issue page. ShowRole RoleDescriptor `xorm:"-"` Review *Review `xorm:"-"` ReviewID int64 `xorm:"index"` Invalidated bool // Reference an issue or pull from another comment, issue or PR // All information is about the origin of the reference RefRepoID int64 `xorm:"index"` // Repo where the referencing RefIssueID int64 `xorm:"index"` RefCommentID int64 `xorm:"index"` // 0 if origin is Issue title or content (or PR's) RefAction references.XRefAction `xorm:"SMALLINT"` // What happens if RefIssueID resolves RefIsPull bool RefRepo *repo_model.Repository `xorm:"-"` RefIssue *Issue `xorm:"-"` RefComment *Comment `xorm:"-"` Commits []*git_model.SignCommitWithStatuses `xorm:"-"` OldCommit string `xorm:"-"` NewCommit string `xorm:"-"` CommitsNum int64 `xorm:"-"` IsForcePush bool `xorm:"-"` } func init() { db.RegisterModel(new(Comment)) } // PushActionContent is content of push pull comment type PushActionContent struct { IsForcePush bool `json:"is_force_push"` CommitIDs []string `json:"commit_ids"` } // LoadIssue loads the issue reference for the comment func (c *Comment) LoadIssue(ctx context.Context) (err error) { if c.Issue != nil { return nil } c.Issue, err = GetIssueByID(ctx, c.IssueID) return err } // BeforeInsert will be invoked by XORM before inserting a record func (c *Comment) BeforeInsert() { c.PatchQuoted = c.Patch if !utf8.ValidString(c.Patch) { c.PatchQuoted = strconv.Quote(c.Patch) } } // BeforeUpdate will be invoked by XORM before updating a record func (c *Comment) BeforeUpdate() { c.PatchQuoted = c.Patch if !utf8.ValidString(c.Patch) { c.PatchQuoted = strconv.Quote(c.Patch) } } // AfterLoad is invoked from XORM after setting the values of all fields of this object. func (c *Comment) AfterLoad() { c.Patch = c.PatchQuoted if len(c.PatchQuoted) > 0 && c.PatchQuoted[0] == '"' { unquoted, err := strconv.Unquote(c.PatchQuoted) if err == nil { c.Patch = unquoted } } } // LoadPoster loads comment poster func (c *Comment) LoadPoster(ctx context.Context) (err error) { if c.Poster != nil { return nil } c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID) if err != nil { if user_model.IsErrUserNotExist(err) { c.PosterID = user_model.GhostUserID c.Poster = user_model.NewGhostUser() } else { log.Error("getUserByID[%d]: %v", c.ID, err) } } return err } // AfterDelete is invoked from XORM after the object is deleted. func (c *Comment) AfterDelete(ctx context.Context) { if c.ID <= 0 { return } _, err := repo_model.DeleteAttachmentsByComment(ctx, c.ID, true) if err != nil { log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err) } } // HTMLURL formats a URL-string to the issue-comment func (c *Comment) HTMLURL(ctx context.Context) string { err := c.LoadIssue(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("LoadIssue(%d): %v", c.IssueID, err) return "" } err = c.Issue.LoadRepo(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" } return c.Issue.HTMLURL() + c.hashLink(ctx) } // Link formats a relative URL-string to the issue-comment func (c *Comment) Link(ctx context.Context) string { err := c.LoadIssue(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("LoadIssue(%d): %v", c.IssueID, err) return "" } err = c.Issue.LoadRepo(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" } return c.Issue.Link() + c.hashLink(ctx) } func (c *Comment) hashLink(ctx context.Context) string { if c.Type == CommentTypeCode { if c.ReviewID == 0 { return "/files#" + c.HashTag() } if c.Review == nil { if err := c.LoadReview(ctx); err != nil { log.Warn("LoadReview(%d): %v", c.ReviewID, err) return "/files#" + c.HashTag() } } if c.Review.Type <= ReviewTypePending { return "/files#" + c.HashTag() } } return "#" + c.HashTag() } // APIURL formats a API-string to the issue-comment func (c *Comment) APIURL(ctx context.Context) string { err := c.LoadIssue(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("LoadIssue(%d): %v", c.IssueID, err) return "" } err = c.Issue.LoadRepo(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" } return fmt.Sprintf("%s/issues/comments/%d", c.Issue.Repo.APIURL(), c.ID) } // IssueURL formats a URL-string to the issue func (c *Comment) IssueURL(ctx context.Context) string { err := c.LoadIssue(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("LoadIssue(%d): %v", c.IssueID, err) return "" } if c.Issue.IsPull { return "" } err = c.Issue.LoadRepo(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return "" } return c.Issue.HTMLURL() } // PRURL formats a URL-string to the pull-request func (c *Comment) PRURL(ctx context.Context) string { err := c.LoadIssue(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("LoadIssue(%d): %v", c.IssueID, err) return "" } err = c.Issue.LoadRepo(ctx) if err != nil { // Silently dropping errors :unamused: log.Error("loadRepo(%d): %v", c.Issue.RepoID, err) return ""