1384 lines · 39.5 KB · Go
RawDownload
// Copyright 2016 The Gogs Authors. All rights reserved.// Copyright 2018 The Gitea Authors. All rights reserved.// Copyright 2024 The Forgejo 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:"-"` 	// If you add new fields that might be used to store abusive content (mainly string fields),	// please also add them in the CommentData struct and the corresponding constructor.} 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)

Lines 1–500 of 1384.