code.gitea.io/gitea@v1.22.3/models/issues/comment.go (about)

     1  // Copyright 2018 The Gitea Authors.
     2  // Copyright 2016 The Gogs Authors.
     3  // All rights reserved.
     4  // SPDX-License-Identifier: MIT
     5  
     6  package issues
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"html/template"
    12  	"strconv"
    13  	"unicode/utf8"
    14  
    15  	"code.gitea.io/gitea/models/db"
    16  	git_model "code.gitea.io/gitea/models/git"
    17  	"code.gitea.io/gitea/models/organization"
    18  	project_model "code.gitea.io/gitea/models/project"
    19  	repo_model "code.gitea.io/gitea/models/repo"
    20  	user_model "code.gitea.io/gitea/models/user"
    21  	"code.gitea.io/gitea/modules/container"
    22  	"code.gitea.io/gitea/modules/gitrepo"
    23  	"code.gitea.io/gitea/modules/json"
    24  	"code.gitea.io/gitea/modules/log"
    25  	"code.gitea.io/gitea/modules/optional"
    26  	"code.gitea.io/gitea/modules/references"
    27  	"code.gitea.io/gitea/modules/structs"
    28  	"code.gitea.io/gitea/modules/timeutil"
    29  	"code.gitea.io/gitea/modules/translation"
    30  	"code.gitea.io/gitea/modules/util"
    31  
    32  	"xorm.io/builder"
    33  )
    34  
    35  // ErrCommentNotExist represents a "CommentNotExist" kind of error.
    36  type ErrCommentNotExist struct {
    37  	ID      int64
    38  	IssueID int64
    39  }
    40  
    41  // IsErrCommentNotExist checks if an error is a ErrCommentNotExist.
    42  func IsErrCommentNotExist(err error) bool {
    43  	_, ok := err.(ErrCommentNotExist)
    44  	return ok
    45  }
    46  
    47  func (err ErrCommentNotExist) Error() string {
    48  	return fmt.Sprintf("comment does not exist [id: %d, issue_id: %d]", err.ID, err.IssueID)
    49  }
    50  
    51  func (err ErrCommentNotExist) Unwrap() error {
    52  	return util.ErrNotExist
    53  }
    54  
    55  // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
    56  type CommentType int
    57  
    58  // CommentTypeUndefined is used to search for comments of any type
    59  const CommentTypeUndefined CommentType = -1
    60  
    61  const (
    62  	CommentTypeComment CommentType = iota // 0 Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0)
    63  
    64  	CommentTypeReopen // 1
    65  	CommentTypeClose  // 2
    66  
    67  	CommentTypeIssueRef   // 3 References.
    68  	CommentTypeCommitRef  // 4 Reference from a commit (not part of a pull request)
    69  	CommentTypeCommentRef // 5 Reference from a comment
    70  	CommentTypePullRef    // 6 Reference from a pull request
    71  
    72  	CommentTypeLabel        // 7 Labels changed
    73  	CommentTypeMilestone    // 8 Milestone changed
    74  	CommentTypeAssignees    // 9 Assignees changed
    75  	CommentTypeChangeTitle  // 10 Change Title
    76  	CommentTypeDeleteBranch // 11 Delete Branch
    77  
    78  	CommentTypeStartTracking    // 12 Start a stopwatch for time tracking
    79  	CommentTypeStopTracking     // 13 Stop a stopwatch for time tracking
    80  	CommentTypeAddTimeManual    // 14 Add time manual for time tracking
    81  	CommentTypeCancelTracking   // 15 Cancel a stopwatch for time tracking
    82  	CommentTypeAddedDeadline    // 16 Added a due date
    83  	CommentTypeModifiedDeadline // 17 Modified the due date
    84  	CommentTypeRemovedDeadline  // 18 Removed a due date
    85  
    86  	CommentTypeAddDependency    // 19 Dependency added
    87  	CommentTypeRemoveDependency // 20 Dependency removed
    88  
    89  	CommentTypeCode   // 21 Comment a line of code
    90  	CommentTypeReview // 22 Reviews a pull request by giving general feedback
    91  
    92  	CommentTypeLock   // 23 Lock an issue, giving only collaborators access
    93  	CommentTypeUnlock // 24 Unlocks a previously locked issue
    94  
    95  	CommentTypeChangeTargetBranch // 25 Change pull request's target branch
    96  
    97  	CommentTypeDeleteTimeManual // 26 Delete time manual for time tracking
    98  
    99  	CommentTypeReviewRequest   // 27 add or remove Request from one
   100  	CommentTypeMergePull       // 28 merge pull request
   101  	CommentTypePullRequestPush // 29 push to PR head branch
   102  
   103  	CommentTypeProject      // 30 Project changed
   104  	CommentTypeProjectBoard // 31 Project board changed
   105  
   106  	CommentTypeDismissReview // 32 Dismiss Review
   107  
   108  	CommentTypeChangeIssueRef // 33 Change issue ref
   109  
   110  	CommentTypePRScheduledToAutoMerge   // 34 pr was scheduled to auto merge when checks succeed
   111  	CommentTypePRUnScheduledToAutoMerge // 35 pr was un scheduled to auto merge when checks succeed
   112  
   113  	CommentTypePin   // 36 pin Issue
   114  	CommentTypeUnpin // 37 unpin Issue
   115  )
   116  
   117  var commentStrings = []string{
   118  	"comment",
   119  	"reopen",
   120  	"close",
   121  	"issue_ref",
   122  	"commit_ref",
   123  	"comment_ref",
   124  	"pull_ref",
   125  	"label",
   126  	"milestone",
   127  	"assignees",
   128  	"change_title",
   129  	"delete_branch",
   130  	"start_tracking",
   131  	"stop_tracking",
   132  	"add_time_manual",
   133  	"cancel_tracking",
   134  	"added_deadline",
   135  	"modified_deadline",
   136  	"removed_deadline",
   137  	"add_dependency",
   138  	"remove_dependency",
   139  	"code",
   140  	"review",
   141  	"lock",
   142  	"unlock",
   143  	"change_target_branch",
   144  	"delete_time_manual",
   145  	"review_request",
   146  	"merge_pull",
   147  	"pull_push",
   148  	"project",
   149  	"project_board",
   150  	"dismiss_review",
   151  	"change_issue_ref",
   152  	"pull_scheduled_merge",
   153  	"pull_cancel_scheduled_merge",
   154  	"pin",
   155  	"unpin",
   156  }
   157  
   158  func (t CommentType) String() string {
   159  	return commentStrings[t]
   160  }
   161  
   162  func AsCommentType(typeName string) CommentType {
   163  	for index, name := range commentStrings {
   164  		if typeName == name {
   165  			return CommentType(index)
   166  		}
   167  	}
   168  	return CommentTypeUndefined
   169  }
   170  
   171  func (t CommentType) HasContentSupport() bool {
   172  	switch t {
   173  	case CommentTypeComment, CommentTypeCode, CommentTypeReview, CommentTypeDismissReview:
   174  		return true
   175  	}
   176  	return false
   177  }
   178  
   179  func (t CommentType) HasAttachmentSupport() bool {
   180  	switch t {
   181  	case CommentTypeComment, CommentTypeCode, CommentTypeReview:
   182  		return true
   183  	}
   184  	return false
   185  }
   186  
   187  func (t CommentType) HasMailReplySupport() bool {
   188  	switch t {
   189  	case CommentTypeComment, CommentTypeCode, CommentTypeReview, CommentTypeDismissReview, CommentTypeReopen, CommentTypeClose, CommentTypeMergePull, CommentTypeAssignees:
   190  		return true
   191  	}
   192  	return false
   193  }
   194  
   195  // RoleInRepo presents the user's participation in the repo
   196  type RoleInRepo string
   197  
   198  // RoleDescriptor defines comment "role" tags
   199  type RoleDescriptor struct {
   200  	IsPoster   bool
   201  	RoleInRepo RoleInRepo
   202  }
   203  
   204  // Enumerate all the role tags.
   205  const (
   206  	RoleRepoOwner                RoleInRepo = "owner"
   207  	RoleRepoMember               RoleInRepo = "member"
   208  	RoleRepoCollaborator         RoleInRepo = "collaborator"
   209  	RoleRepoFirstTimeContributor RoleInRepo = "first_time_contributor"
   210  	RoleRepoContributor          RoleInRepo = "contributor"
   211  )
   212  
   213  // LocaleString returns the locale string name of the role
   214  func (r RoleInRepo) LocaleString(lang translation.Locale) string {
   215  	return lang.TrString("repo.issues.role." + string(r))
   216  }
   217  
   218  // LocaleHelper returns the locale tooltip of the role
   219  func (r RoleInRepo) LocaleHelper(lang translation.Locale) string {
   220  	return lang.TrString("repo.issues.role." + string(r) + "_helper")
   221  }
   222  
   223  // Comment represents a comment in commit and issue page.
   224  type Comment struct {
   225  	ID               int64            `xorm:"pk autoincr"`
   226  	Type             CommentType      `xorm:"INDEX"`
   227  	PosterID         int64            `xorm:"INDEX"`
   228  	Poster           *user_model.User `xorm:"-"`
   229  	OriginalAuthor   string
   230  	OriginalAuthorID int64
   231  	IssueID          int64  `xorm:"INDEX"`
   232  	Issue            *Issue `xorm:"-"`
   233  	LabelID          int64
   234  	Label            *Label   `xorm:"-"`
   235  	AddedLabels      []*Label `xorm:"-"`
   236  	RemovedLabels    []*Label `xorm:"-"`
   237  	OldProjectID     int64
   238  	ProjectID        int64
   239  	OldProject       *project_model.Project `xorm:"-"`
   240  	Project          *project_model.Project `xorm:"-"`
   241  	OldMilestoneID   int64
   242  	MilestoneID      int64
   243  	OldMilestone     *Milestone `xorm:"-"`
   244  	Milestone        *Milestone `xorm:"-"`
   245  	TimeID           int64
   246  	Time             *TrackedTime `xorm:"-"`
   247  	AssigneeID       int64
   248  	RemovedAssignee  bool
   249  	Assignee         *user_model.User   `xorm:"-"`
   250  	AssigneeTeamID   int64              `xorm:"NOT NULL DEFAULT 0"`
   251  	AssigneeTeam     *organization.Team `xorm:"-"`
   252  	ResolveDoerID    int64
   253  	ResolveDoer      *user_model.User `xorm:"-"`
   254  	OldTitle         string
   255  	NewTitle         string
   256  	OldRef           string
   257  	NewRef           string
   258  	DependentIssueID int64  `xorm:"index"` // This is used by issue_service.deleteIssue
   259  	DependentIssue   *Issue `xorm:"-"`
   260  
   261  	CommitID        int64
   262  	Line            int64 // - previous line / + proposed line
   263  	TreePath        string
   264  	Content         string        `xorm:"LONGTEXT"`
   265  	RenderedContent template.HTML `xorm:"-"`
   266  
   267  	// Path represents the 4 lines of code cemented by this comment
   268  	Patch       string `xorm:"-"`
   269  	PatchQuoted string `xorm:"LONGTEXT patch"`
   270  
   271  	CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
   272  	UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
   273  
   274  	// Reference issue in commit message
   275  	CommitSHA string `xorm:"VARCHAR(64)"`
   276  
   277  	Attachments []*repo_model.Attachment `xorm:"-"`
   278  	Reactions   ReactionList             `xorm:"-"`
   279  
   280  	// For view issue page.
   281  	ShowRole RoleDescriptor `xorm:"-"`
   282  
   283  	Review      *Review `xorm:"-"`
   284  	ReviewID    int64   `xorm:"index"`
   285  	Invalidated bool
   286  
   287  	// Reference an issue or pull from another comment, issue or PR
   288  	// All information is about the origin of the reference
   289  	RefRepoID    int64                 `xorm:"index"` // Repo where the referencing
   290  	RefIssueID   int64                 `xorm:"index"`
   291  	RefCommentID int64                 `xorm:"index"`    // 0 if origin is Issue title or content (or PR's)
   292  	RefAction    references.XRefAction `xorm:"SMALLINT"` // What happens if RefIssueID resolves
   293  	RefIsPull    bool
   294  
   295  	RefRepo    *repo_model.Repository `xorm:"-"`
   296  	RefIssue   *Issue                 `xorm:"-"`
   297  	RefComment *Comment               `xorm:"-"`
   298  
   299  	Commits     []*git_model.SignCommitWithStatuses `xorm:"-"`
   300  	OldCommit   string                              `xorm:"-"`
   301  	NewCommit   string                              `xorm:"-"`
   302  	CommitsNum  int64                               `xorm:"-"`
   303  	IsForcePush bool                                `xorm:"-"`
   304  }
   305  
   306  func init() {
   307  	db.RegisterModel(new(Comment))
   308  }
   309  
   310  // PushActionContent is content of push pull comment
   311  type PushActionContent struct {
   312  	IsForcePush bool     `json:"is_force_push"`
   313  	CommitIDs   []string `json:"commit_ids"`
   314  }
   315  
   316  // LoadIssue loads the issue reference for the comment
   317  func (c *Comment) LoadIssue(ctx context.Context) (err error) {
   318  	if c.Issue != nil {
   319  		return nil
   320  	}
   321  	c.Issue, err = GetIssueByID(ctx, c.IssueID)
   322  	return err
   323  }
   324  
   325  // BeforeInsert will be invoked by XORM before inserting a record
   326  func (c *Comment) BeforeInsert() {
   327  	c.PatchQuoted = c.Patch
   328  	if !utf8.ValidString(c.Patch) {
   329  		c.PatchQuoted = strconv.Quote(c.Patch)
   330  	}
   331  }
   332  
   333  // BeforeUpdate will be invoked by XORM before updating a record
   334  func (c *Comment) BeforeUpdate() {
   335  	c.PatchQuoted = c.Patch
   336  	if !utf8.ValidString(c.Patch) {
   337  		c.PatchQuoted = strconv.Quote(c.Patch)
   338  	}
   339  }
   340  
   341  // AfterLoad is invoked from XORM after setting the values of all fields of this object.
   342  func (c *Comment) AfterLoad() {
   343  	c.Patch = c.PatchQuoted
   344  	if len(c.PatchQuoted) > 0 && c.PatchQuoted[0] == '"' {
   345  		unquoted, err := strconv.Unquote(c.PatchQuoted)
   346  		if err == nil {
   347  			c.Patch = unquoted
   348  		}
   349  	}
   350  }
   351  
   352  // LoadPoster loads comment poster
   353  func (c *Comment) LoadPoster(ctx context.Context) (err error) {
   354  	if c.Poster != nil {
   355  		return nil
   356  	}
   357  
   358  	c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID)
   359  	if err != nil {
   360  		if user_model.IsErrUserNotExist(err) {
   361  			c.PosterID = user_model.GhostUserID
   362  			c.Poster = user_model.NewGhostUser()
   363  		} else {
   364  			log.Error("getUserByID[%d]: %v", c.ID, err)
   365  		}
   366  	}
   367  	return err
   368  }
   369  
   370  // AfterDelete is invoked from XORM after the object is deleted.
   371  func (c *Comment) AfterDelete(ctx context.Context) {
   372  	if c.ID <= 0 {
   373  		return
   374  	}
   375  
   376  	_, err := repo_model.DeleteAttachmentsByComment(ctx, c.ID, true)
   377  	if err != nil {
   378  		log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err)
   379  	}
   380  }
   381  
   382  // HTMLURL formats a URL-string to the issue-comment
   383  func (c *Comment) HTMLURL(ctx context.Context) string {
   384  	err := c.LoadIssue(ctx)
   385  	if err != nil { // Silently dropping errors :unamused:
   386  		log.Error("LoadIssue(%d): %v", c.IssueID, err)
   387  		return ""
   388  	}
   389  	err = c.Issue.LoadRepo(ctx)
   390  	if err != nil { // Silently dropping errors :unamused:
   391  		log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
   392  		return ""
   393  	}
   394  	return c.Issue.HTMLURL() + c.hashLink(ctx)
   395  }
   396  
   397  // Link formats a relative URL-string to the issue-comment
   398  func (c *Comment) Link(ctx context.Context) string {
   399  	err := c.LoadIssue(ctx)
   400  	if err != nil { // Silently dropping errors :unamused:
   401  		log.Error("LoadIssue(%d): %v", c.IssueID, err)
   402  		return ""
   403  	}
   404  	err = c.Issue.LoadRepo(ctx)
   405  	if err != nil { // Silently dropping errors :unamused:
   406  		log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
   407  		return ""
   408  	}
   409  	return c.Issue.Link() + c.hashLink(ctx)
   410  }
   411  
   412  func (c *Comment) hashLink(ctx context.Context) string {
   413  	if c.Type == CommentTypeCode {
   414  		if c.ReviewID == 0 {
   415  			return "/files#" + c.HashTag()
   416  		}
   417  		if c.Review == nil {
   418  			if err := c.LoadReview(ctx); err != nil {
   419  				log.Warn("LoadReview(%d): %v", c.ReviewID, err)
   420  				return "/files#" + c.HashTag()
   421  			}
   422  		}
   423  		if c.Review.Type <= ReviewTypePending {
   424  			return "/files#" + c.HashTag()
   425  		}
   426  	}
   427  	return "#" + c.HashTag()
   428  }
   429  
   430  // APIURL formats a API-string to the issue-comment
   431  func (c *Comment) APIURL(ctx context.Context) string {
   432  	err := c.LoadIssue(ctx)
   433  	if err != nil { // Silently dropping errors :unamused:
   434  		log.Error("LoadIssue(%d): %v", c.IssueID, err)
   435  		return ""
   436  	}
   437  	err = c.Issue.LoadRepo(ctx)
   438  	if err != nil { // Silently dropping errors :unamused:
   439  		log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
   440  		return ""
   441  	}
   442  
   443  	return fmt.Sprintf("%s/issues/comments/%d", c.Issue.Repo.APIURL(), c.ID)
   444  }
   445  
   446  // IssueURL formats a URL-string to the issue
   447  func (c *Comment) IssueURL(ctx context.Context) string {
   448  	err := c.LoadIssue(ctx)
   449  	if err != nil { // Silently dropping errors :unamused:
   450  		log.Error("LoadIssue(%d): %v", c.IssueID, err)
   451  		return ""
   452  	}
   453  
   454  	if c.Issue.IsPull {
   455  		return ""
   456  	}
   457  
   458  	err = c.Issue.LoadRepo(ctx)
   459  	if err != nil { // Silently dropping errors :unamused:
   460  		log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
   461  		return ""
   462  	}
   463  	return c.Issue.HTMLURL()
   464  }
   465  
   466  // PRURL formats a URL-string to the pull-request
   467  func (c *Comment) PRURL(ctx context.Context) string {
   468  	err := c.LoadIssue(ctx)
   469  	if err != nil { // Silently dropping errors :unamused:
   470  		log.Error("LoadIssue(%d): %v", c.IssueID, err)
   471  		return ""
   472  	}
   473  
   474  	err = c.Issue.LoadRepo(ctx)
   475  	if err != nil { // Silently dropping errors :unamused:
   476  		log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
   477  		return ""
   478  	}
   479  
   480  	if !c.Issue.IsPull {
   481  		return ""
   482  	}
   483  	return c.Issue.HTMLURL()
   484  }
   485  
   486  // CommentHashTag returns unique hash tag for comment id.
   487  func CommentHashTag(id int64) string {
   488  	return fmt.Sprintf("issuecomment-%d", id)
   489  }
   490  
   491  // HashTag returns unique hash tag for comment.
   492  func (c *Comment) HashTag() string {
   493  	return CommentHashTag(c.ID)
   494  }
   495  
   496  // EventTag returns unique event hash tag for comment.
   497  func (c *Comment) EventTag() string {
   498  	return fmt.Sprintf("event-%d", c.ID)
   499  }
   500  
   501  // LoadLabel if comment.Type is CommentTypeLabel, then load Label
   502  func (c *Comment) LoadLabel(ctx context.Context) error {
   503  	var label Label
   504  	has, err := db.GetEngine(ctx).ID(c.LabelID).Get(&label)
   505  	if err != nil {
   506  		return err
   507  	} else if has {
   508  		c.Label = &label
   509  	} else {
   510  		// Ignore Label is deleted, but not clear this table
   511  		log.Warn("Commit %d cannot load label %d", c.ID, c.LabelID)
   512  	}
   513  
   514  	return nil
   515  }
   516  
   517  // LoadProject if comment.Type is CommentTypeProject, then load project.
   518  func (c *Comment) LoadProject(ctx context.Context) error {
   519  	if c.OldProjectID > 0 {
   520  		var oldProject project_model.Project
   521  		has, err := db.GetEngine(ctx).ID(c.OldProjectID).Get(&oldProject)
   522  		if err != nil {
   523  			return err
   524  		} else if has {
   525  			c.OldProject = &oldProject
   526  		}
   527  	}
   528  
   529  	if c.ProjectID > 0 {
   530  		var project project_model.Project
   531  		has, err := db.GetEngine(ctx).ID(c.ProjectID).Get(&project)
   532  		if err != nil {
   533  			return err
   534  		} else if has {
   535  			c.Project = &project
   536  		}
   537  	}
   538  
   539  	return nil
   540  }
   541  
   542  // LoadMilestone if comment.Type is CommentTypeMilestone, then load milestone
   543  func (c *Comment) LoadMilestone(ctx context.Context) error {
   544  	if c.OldMilestoneID > 0 {
   545  		var oldMilestone Milestone
   546  		has, err := db.GetEngine(ctx).ID(c.OldMilestoneID).Get(&oldMilestone)
   547  		if err != nil {
   548  			return err
   549  		} else if has {
   550  			c.OldMilestone = &oldMilestone
   551  		}
   552  	}
   553  
   554  	if c.MilestoneID > 0 {
   555  		var milestone Milestone
   556  		has, err := db.GetEngine(ctx).ID(c.MilestoneID).Get(&milestone)
   557  		if err != nil {
   558  			return err
   559  		} else if has {
   560  			c.Milestone = &milestone
   561  		}
   562  	}
   563  	return nil
   564  }
   565  
   566  // LoadAttachments loads attachments (it never returns error, the error during `GetAttachmentsByCommentIDCtx` is ignored)
   567  func (c *Comment) LoadAttachments(ctx context.Context) error {
   568  	if len(c.Attachments) > 0 {
   569  		return nil
   570  	}
   571  
   572  	var err error
   573  	c.Attachments, err = repo_model.GetAttachmentsByCommentID(ctx, c.ID)
   574  	if err != nil {
   575  		log.Error("getAttachmentsByCommentID[%d]: %v", c.ID, err)
   576  	}
   577  	return nil
   578  }
   579  
   580  // UpdateAttachments update attachments by UUIDs for the comment
   581  func (c *Comment) UpdateAttachments(ctx context.Context, uuids []string) error {
   582  	ctx, committer, err := db.TxContext(ctx)
   583  	if err != nil {
   584  		return err
   585  	}
   586  	defer committer.Close()
   587  
   588  	attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids)
   589  	if err != nil {
   590  		return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
   591  	}
   592  	for i := 0; i < len(attachments); i++ {
   593  		attachments[i].IssueID = c.IssueID
   594  		attachments[i].CommentID = c.ID
   595  		if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
   596  			return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)
   597  		}
   598  	}
   599  	return committer.Commit()
   600  }
   601  
   602  // LoadAssigneeUserAndTeam if comment.Type is CommentTypeAssignees, then load assignees
   603  func (c *Comment) LoadAssigneeUserAndTeam(ctx context.Context) error {
   604  	var err error
   605  
   606  	if c.AssigneeID > 0 && c.Assignee == nil {
   607  		c.Assignee, err = user_model.GetUserByID(ctx, c.AssigneeID)
   608  		if err != nil {
   609  			if !user_model.IsErrUserNotExist(err) {
   610  				return err
   611  			}
   612  			c.Assignee = user_model.NewGhostUser()
   613  		}
   614  	} else if c.AssigneeTeamID > 0 && c.AssigneeTeam == nil {
   615  		if err = c.LoadIssue(ctx); err != nil {
   616  			return err
   617  		}
   618  
   619  		if err = c.Issue.LoadRepo(ctx); err != nil {
   620  			return err
   621  		}
   622  
   623  		if err = c.Issue.Repo.LoadOwner(ctx); err != nil {
   624  			return err
   625  		}
   626  
   627  		if c.Issue.Repo.Owner.IsOrganization() {
   628  			c.AssigneeTeam, err = organization.GetTeamByID(ctx, c.AssigneeTeamID)
   629  			if err != nil && !organization.IsErrTeamNotExist(err) {
   630  				return err
   631  			}
   632  		}
   633  	}
   634  	return nil
   635  }
   636  
   637  // LoadResolveDoer if comment.Type is CommentTypeCode and ResolveDoerID not zero, then load resolveDoer
   638  func (c *Comment) LoadResolveDoer(ctx context.Context) (err error) {
   639  	if c.ResolveDoerID == 0 || c.Type != CommentTypeCode {
   640  		return nil
   641  	}
   642  	c.ResolveDoer, err = user_model.GetUserByID(ctx, c.ResolveDoerID)
   643  	if err != nil {
   644  		if user_model.IsErrUserNotExist(err) {
   645  			c.ResolveDoer = user_model.NewGhostUser()
   646  			err = nil
   647  		}
   648  	}
   649  	return err
   650  }
   651  
   652  // IsResolved check if an code comment is resolved
   653  func (c *Comment) IsResolved() bool {
   654  	return c.ResolveDoerID != 0 && c.Type == CommentTypeCode
   655  }
   656  
   657  // LoadDepIssueDetails loads Dependent Issue Details
   658  func (c *Comment) LoadDepIssueDetails(ctx context.Context) (err error) {
   659  	if c.DependentIssueID <= 0 || c.DependentIssue != nil {
   660  		return nil
   661  	}
   662  	c.DependentIssue, err = GetIssueByID(ctx, c.DependentIssueID)
   663  	return err
   664  }
   665  
   666  // LoadTime loads the associated time for a CommentTypeAddTimeManual
   667  func (c *Comment) LoadTime(ctx context.Context) error {
   668  	if c.Time != nil || c.TimeID == 0 {
   669  		return nil
   670  	}
   671  	var err error
   672  	c.Time, err = GetTrackedTimeByID(ctx, c.TimeID)
   673  	return err
   674  }
   675  
   676  // LoadReactions loads comment reactions
   677  func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository) (err error) {
   678  	if c.Reactions != nil {
   679  		return nil
   680  	}
   681  	c.Reactions, _, err = FindReactions(ctx, FindReactionsOptions{
   682  		IssueID:   c.IssueID,
   683  		CommentID: c.ID,
   684  	})
   685  	if err != nil {
   686  		return err
   687  	}
   688  	// Load reaction user data
   689  	if _, err := c.Reactions.LoadUsers(ctx, repo); err != nil {
   690  		return err
   691  	}
   692  	return nil
   693  }
   694  
   695  func (c *Comment) loadReview(ctx context.Context) (err error) {
   696  	if c.ReviewID == 0 {
   697  		return nil
   698  	}
   699  	if c.Review == nil {
   700  		if c.Review, err = GetReviewByID(ctx, c.ReviewID); err != nil {
   701  			// review request which has been replaced by actual reviews doesn't exist in database anymore, so ignorem them.
   702  			if c.Type == CommentTypeReviewRequest {
   703  				return nil
   704  			}
   705  			return err
   706  		}
   707  	}
   708  	c.Review.Issue = c.Issue
   709  	return nil
   710  }
   711  
   712  // LoadReview loads the associated review
   713  func (c *Comment) LoadReview(ctx context.Context) error {
   714  	return c.loadReview(ctx)
   715  }
   716  
   717  // DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
   718  func (c *Comment) DiffSide() string {
   719  	if c.Line < 0 {
   720  		return "previous"
   721  	}
   722  	return "proposed"
   723  }
   724  
   725  // UnsignedLine returns the LOC of the code comment without + or -
   726  func (c *Comment) UnsignedLine() uint64 {
   727  	if c.Line < 0 {
   728  		return uint64(c.Line * -1)
   729  	}
   730  	return uint64(c.Line)
   731  }
   732  
   733  // CodeCommentLink returns the url to a comment in code
   734  func (c *Comment) CodeCommentLink(ctx context.Context) string {
   735  	err := c.LoadIssue(ctx)
   736  	if err != nil { // Silently dropping errors :unamused:
   737  		log.Error("LoadIssue(%d): %v", c.IssueID, err)
   738  		return ""
   739  	}
   740  	err = c.Issue.LoadRepo(ctx)
   741  	if err != nil { // Silently dropping errors :unamused:
   742  		log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
   743  		return ""
   744  	}
   745  	return fmt.Sprintf("%s/files#%s", c.Issue.Link(), c.HashTag())
   746  }
   747  
   748  // LoadPushCommits Load push commits
   749  func (c *Comment) LoadPushCommits(ctx context.Context) (err error) {
   750  	if c.Content == "" || c.Commits != nil || c.Type != CommentTypePullRequestPush {
   751  		return nil
   752  	}
   753  
   754  	var data PushActionContent
   755  
   756  	err = json.Unmarshal([]byte(c.Content), &data)
   757  	if err != nil {
   758  		return err
   759  	}
   760  
   761  	c.IsForcePush = data.IsForcePush
   762  
   763  	if c.IsForcePush {
   764  		if len(data.CommitIDs) != 2 {
   765  			return nil
   766  		}
   767  		c.OldCommit = data.CommitIDs[0]
   768  		c.NewCommit = data.CommitIDs[1]
   769  	} else {
   770  		gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, c.Issue.Repo)
   771  		if err != nil {
   772  			return err
   773  		}
   774  		defer closer.Close()
   775  
   776  		c.Commits = git_model.ConvertFromGitCommit(ctx, gitRepo.GetCommitsFromIDs(data.CommitIDs), c.Issue.Repo)
   777  		c.CommitsNum = int64(len(c.Commits))
   778  	}
   779  
   780  	return err
   781  }
   782  
   783  // CreateComment creates comment with context
   784  func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, err error) {
   785  	ctx, committer, err := db.TxContext(ctx)
   786  	if err != nil {
   787  		return nil, err
   788  	}
   789  	defer committer.Close()
   790  
   791  	e := db.GetEngine(ctx)
   792  	var LabelID int64
   793  	if opts.Label != nil {
   794  		LabelID = opts.Label.ID
   795  	}
   796  
   797  	comment := &Comment{
   798  		Type:             opts.Type,
   799  		PosterID:         opts.Doer.ID,
   800  		Poster:           opts.Doer,
   801  		IssueID:          opts.Issue.ID,
   802  		LabelID:          LabelID,
   803  		OldMilestoneID:   opts.OldMilestoneID,
   804  		MilestoneID:      opts.MilestoneID,
   805  		OldProjectID:     opts.OldProjectID,
   806  		ProjectID:        opts.ProjectID,
   807  		TimeID:           opts.TimeID,
   808  		RemovedAssignee:  opts.RemovedAssignee,
   809  		AssigneeID:       opts.AssigneeID,
   810  		AssigneeTeamID:   opts.AssigneeTeamID,
   811  		CommitID:         opts.CommitID,
   812  		CommitSHA:        opts.CommitSHA,
   813  		Line:             opts.LineNum,
   814  		Content:          opts.Content,
   815  		OldTitle:         opts.OldTitle,
   816  		NewTitle:         opts.NewTitle,
   817  		OldRef:           opts.OldRef,
   818  		NewRef:           opts.NewRef,
   819  		DependentIssueID: opts.DependentIssueID,
   820  		TreePath:         opts.TreePath,
   821  		ReviewID:         opts.ReviewID,
   822  		Patch:            opts.Patch,
   823  		RefRepoID:        opts.RefRepoID,
   824  		RefIssueID:       opts.RefIssueID,
   825  		RefCommentID:     opts.RefCommentID,
   826  		RefAction:        opts.RefAction,
   827  		RefIsPull:        opts.RefIsPull,
   828  		IsForcePush:      opts.IsForcePush,
   829  		Invalidated:      opts.Invalidated,
   830  	}
   831  	if _, err = e.Insert(comment); err != nil {
   832  		return nil, err
   833  	}
   834  
   835  	if err = opts.Repo.LoadOwner(ctx); err != nil {
   836  		return nil, err
   837  	}
   838  
   839  	if err = updateCommentInfos(ctx, opts, comment); err != nil {
   840  		return nil, err
   841  	}
   842  
   843  	if err = comment.AddCrossReferences(ctx, opts.Doer, false); err != nil {
   844  		return nil, err
   845  	}
   846  	if err = committer.Commit(); err != nil {
   847  		return nil, err
   848  	}
   849  	return comment, nil
   850  }
   851  
   852  func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment *Comment) (err error) {
   853  	// Check comment type.
   854  	switch opts.Type {
   855  	case CommentTypeCode:
   856  		if err = updateAttachments(ctx, opts, comment); err != nil {
   857  			return err
   858  		}
   859  		if comment.ReviewID != 0 {
   860  			if comment.Review == nil {
   861  				if err := comment.loadReview(ctx); err != nil {
   862  					return err
   863  				}
   864  			}
   865  			if comment.Review.Type <= ReviewTypePending {
   866  				return nil
   867  			}
   868  		}
   869  		fallthrough
   870  	case CommentTypeComment:
   871  		if _, err = db.Exec(ctx, "UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
   872  			return err
   873  		}
   874  		fallthrough
   875  	case CommentTypeReview:
   876  		if err = updateAttachments(ctx, opts, comment); err != nil {
   877  			return err
   878  		}
   879  	case CommentTypeReopen, CommentTypeClose:
   880  		if err = repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.Issue.IsPull, true); err != nil {
   881  			return err
   882  		}
   883  	}
   884  	// update the issue's updated_unix column
   885  	return UpdateIssueCols(ctx, opts.Issue, "updated_unix")
   886  }
   887  
   888  func updateAttachments(ctx context.Context, opts *CreateCommentOptions, comment *Comment) error {
   889  	attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments)
   890  	if err != nil {
   891  		return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err)
   892  	}
   893  	for i := range attachments {
   894  		attachments[i].IssueID = opts.Issue.ID
   895  		attachments[i].CommentID = comment.ID
   896  		// No assign value could be 0, so ignore AllCols().
   897  		if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil {
   898  			return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err)
   899  		}
   900  	}
   901  	comment.Attachments = attachments
   902  	return nil
   903  }
   904  
   905  func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) {
   906  	var content string
   907  	var commentType CommentType
   908  
   909  	// newDeadline = 0 means deleting
   910  	if newDeadlineUnix == 0 {
   911  		commentType = CommentTypeRemovedDeadline
   912  		content = issue.DeadlineUnix.FormatDate()
   913  	} else if issue.DeadlineUnix == 0 {
   914  		// Check if the new date was added or modified
   915  		// If the actual deadline is 0 => deadline added
   916  		commentType = CommentTypeAddedDeadline
   917  		content = newDeadlineUnix.FormatDate()
   918  	} else { // Otherwise modified
   919  		commentType = CommentTypeModifiedDeadline
   920  		content = newDeadlineUnix.FormatDate() + "|" + issue.DeadlineUnix.FormatDate()
   921  	}
   922  
   923  	if err := issue.LoadRepo(ctx); err != nil {
   924  		return nil, err
   925  	}
   926  
   927  	opts := &CreateCommentOptions{
   928  		Type:    commentType,
   929  		Doer:    doer,
   930  		Repo:    issue.Repo,
   931  		Issue:   issue,
   932  		Content: content,
   933  	}
   934  	comment, err := CreateComment(ctx, opts)
   935  	if err != nil {
   936  		return nil, err
   937  	}
   938  	return comment, nil
   939  }
   940  
   941  // Creates issue dependency comment
   942  func createIssueDependencyComment(ctx context.Context, doer *user_model.User, issue, dependentIssue *Issue, add bool) (err error) {
   943  	cType := CommentTypeAddDependency
   944  	if !add {
   945  		cType = CommentTypeRemoveDependency
   946  	}
   947  	if err = issue.LoadRepo(ctx); err != nil {
   948  		return err
   949  	}
   950  
   951  	// Make two comments, one in each issue
   952  	opts := &CreateCommentOptions{
   953  		Type:             cType,
   954  		Doer:             doer,
   955  		Repo:             issue.Repo,
   956  		Issue:            issue,
   957  		DependentIssueID: dependentIssue.ID,
   958  	}
   959  	if _, err = CreateComment(ctx, opts); err != nil {
   960  		return err
   961  	}
   962  
   963  	opts = &CreateCommentOptions{
   964  		Type:             cType,
   965  		Doer:             doer,
   966  		Repo:             issue.Repo,
   967  		Issue:            dependentIssue,
   968  		DependentIssueID: issue.ID,
   969  	}
   970  	_, err = CreateComment(ctx, opts)
   971  	return err
   972  }
   973  
   974  // CreateCommentOptions defines options for creating comment
   975  type CreateCommentOptions struct {
   976  	Type  CommentType
   977  	Doer  *user_model.User
   978  	Repo  *repo_model.Repository
   979  	Issue *Issue
   980  	Label *Label
   981  
   982  	DependentIssueID int64
   983  	OldMilestoneID   int64
   984  	MilestoneID      int64
   985  	OldProjectID     int64
   986  	ProjectID        int64
   987  	TimeID           int64
   988  	AssigneeID       int64
   989  	AssigneeTeamID   int64
   990  	RemovedAssignee  bool
   991  	OldTitle         string
   992  	NewTitle         string
   993  	OldRef           string
   994  	NewRef           string
   995  	CommitID         int64
   996  	CommitSHA        string
   997  	Patch            string
   998  	LineNum          int64
   999  	TreePath         string
  1000  	ReviewID         int64
  1001  	Content          string
  1002  	Attachments      []string // UUIDs of attachments
  1003  	RefRepoID        int64
  1004  	RefIssueID       int64
  1005  	RefCommentID     int64
  1006  	RefAction        references.XRefAction
  1007  	RefIsPull        bool
  1008  	IsForcePush      bool
  1009  	Invalidated      bool
  1010  }
  1011  
  1012  // GetCommentByID returns the comment by given ID.
  1013  func GetCommentByID(ctx context.Context, id int64) (*Comment, error) {
  1014  	c := new(Comment)
  1015  	has, err := db.GetEngine(ctx).ID(id).Get(c)
  1016  	if err != nil {
  1017  		return nil, err
  1018  	} else if !has {
  1019  		return nil, ErrCommentNotExist{id, 0}
  1020  	}
  1021  	return c, nil
  1022  }
  1023  
  1024  // FindCommentsOptions describes the conditions to Find comments
  1025  type FindCommentsOptions struct {
  1026  	db.ListOptions
  1027  	RepoID      int64
  1028  	IssueID     int64
  1029  	ReviewID    int64
  1030  	Since       int64
  1031  	Before      int64
  1032  	Line        int64
  1033  	TreePath    string
  1034  	Type        CommentType
  1035  	IssueIDs    []int64
  1036  	Invalidated optional.Option[bool]
  1037  	IsPull      optional.Option[bool]
  1038  }
  1039  
  1040  // ToConds implements FindOptions interface
  1041  func (opts FindCommentsOptions) ToConds() builder.Cond {
  1042  	cond := builder.NewCond()
  1043  	if opts.RepoID > 0 {
  1044  		cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID})
  1045  	}
  1046  	if opts.IssueID > 0 {
  1047  		cond = cond.And(builder.Eq{"comment.issue_id": opts.IssueID})
  1048  	} else if len(opts.IssueIDs) > 0 {
  1049  		cond = cond.And(builder.In("comment.issue_id", opts.IssueIDs))
  1050  	}
  1051  	if opts.ReviewID > 0 {
  1052  		cond = cond.And(builder.Eq{"comment.review_id": opts.ReviewID})
  1053  	}
  1054  	if opts.Since > 0 {
  1055  		cond = cond.And(builder.Gte{"comment.updated_unix": opts.Since})
  1056  	}
  1057  	if opts.Before > 0 {
  1058  		cond = cond.And(builder.Lte{"comment.updated_unix": opts.Before})
  1059  	}
  1060  	if opts.Type != CommentTypeUndefined {
  1061  		cond = cond.And(builder.Eq{"comment.type": opts.Type})
  1062  	}
  1063  	if opts.Line != 0 {
  1064  		cond = cond.And(builder.Eq{"comment.line": opts.Line})
  1065  	}
  1066  	if len(opts.TreePath) > 0 {
  1067  		cond = cond.And(builder.Eq{"comment.tree_path": opts.TreePath})
  1068  	}
  1069  	if opts.Invalidated.Has() {
  1070  		cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.Value()})
  1071  	}
  1072  	if opts.IsPull.Has() {
  1073  		cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.Value()})
  1074  	}
  1075  	return cond
  1076  }
  1077  
  1078  // FindComments returns all comments according options
  1079  func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) {
  1080  	comments := make([]*Comment, 0, 10)
  1081  	sess := db.GetEngine(ctx).Where(opts.ToConds())
  1082  	if opts.RepoID > 0 || opts.IsPull.Has() {
  1083  		sess.Join("INNER", "issue", "issue.id = comment.issue_id")
  1084  	}
  1085  
  1086  	if opts.Page != 0 {
  1087  		sess = db.SetSessionPagination(sess, opts)
  1088  	}
  1089  
  1090  	// WARNING: If you change this order you will need to fix createCodeComment
  1091  
  1092  	return comments, sess.
  1093  		Asc("comment.created_unix").
  1094  		Asc("comment.id").
  1095  		Find(&comments)
  1096  }
  1097  
  1098  // CountComments count all comments according options by ignoring pagination
  1099  func CountComments(ctx context.Context, opts *FindCommentsOptions) (int64, error) {
  1100  	sess := db.GetEngine(ctx).Where(opts.ToConds())
  1101  	if opts.RepoID > 0 {
  1102  		sess.Join("INNER", "issue", "issue.id = comment.issue_id")
  1103  	}
  1104  	return sess.Count(&Comment{})
  1105  }
  1106  
  1107  // UpdateCommentInvalidate updates comment invalidated column
  1108  func UpdateCommentInvalidate(ctx context.Context, c *Comment) error {
  1109  	_, err := db.GetEngine(ctx).ID(c.ID).Cols("invalidated").Update(c)
  1110  	return err
  1111  }
  1112  
  1113  // UpdateComment updates information of comment.
  1114  func UpdateComment(ctx context.Context, c *Comment, doer *user_model.User) error {
  1115  	ctx, committer, err := db.TxContext(ctx)
  1116  	if err != nil {
  1117  		return err
  1118  	}
  1119  	defer committer.Close()
  1120  	sess := db.GetEngine(ctx)
  1121  
  1122  	if _, err := sess.ID(c.ID).AllCols().Update(c); err != nil {
  1123  		return err
  1124  	}
  1125  	if err := c.LoadIssue(ctx); err != nil {
  1126  		return err
  1127  	}
  1128  	if err := c.AddCrossReferences(ctx, doer, true); err != nil {
  1129  		return err
  1130  	}
  1131  	if err := committer.Commit(); err != nil {
  1132  		return fmt.Errorf("Commit: %w", err)
  1133  	}
  1134  
  1135  	return nil
  1136  }
  1137  
  1138  // DeleteComment deletes the comment
  1139  func DeleteComment(ctx context.Context, comment *Comment) error {
  1140  	e := db.GetEngine(ctx)
  1141  	if _, err := e.ID(comment.ID).NoAutoCondition().Delete(comment); err != nil {
  1142  		return err
  1143  	}
  1144  
  1145  	if _, err := db.DeleteByBean(ctx, &ContentHistory{
  1146  		CommentID: comment.ID,
  1147  	}); err != nil {
  1148  		return err
  1149  	}
  1150  
  1151  	if comment.Type == CommentTypeComment {
  1152  		if _, err := e.ID(comment.IssueID).Decr("num_comments").Update(new(Issue)); err != nil {
  1153  			return err
  1154  		}
  1155  	}
  1156  	if _, err := e.Table("action").
  1157  		Where("comment_id = ?", comment.ID).
  1158  		Update(map[string]any{
  1159  			"is_deleted": true,
  1160  		}); err != nil {
  1161  		return err
  1162  	}
  1163  
  1164  	if err := comment.neuterCrossReferences(ctx); err != nil {
  1165  		return err
  1166  	}
  1167  
  1168  	return DeleteReaction(ctx, &ReactionOptions{CommentID: comment.ID})
  1169  }
  1170  
  1171  // UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id
  1172  func UpdateCommentsMigrationsByType(ctx context.Context, tp structs.GitServiceType, originalAuthorID string, posterID int64) error {
  1173  	_, err := db.GetEngine(ctx).Table("comment").
  1174  		Join("INNER", "issue", "issue.id = comment.issue_id").
  1175  		Join("INNER", "repository", "issue.repo_id = repository.id").
  1176  		Where("repository.original_service_type = ?", tp).
  1177  		And("comment.original_author_id = ?", originalAuthorID).
  1178  		Update(map[string]any{
  1179  			"poster_id":          posterID,
  1180  			"original_author":    "",
  1181  			"original_author_id": 0,
  1182  		})
  1183  	return err
  1184  }
  1185  
  1186  // CreateAutoMergeComment is a internal function, only use it for CommentTypePRScheduledToAutoMerge and CommentTypePRUnScheduledToAutoMerge CommentTypes
  1187  func CreateAutoMergeComment(ctx context.Context, typ CommentType, pr *PullRequest, doer *user_model.User) (comment *Comment, err error) {
  1188  	if typ != CommentTypePRScheduledToAutoMerge && typ != CommentTypePRUnScheduledToAutoMerge {
  1189  		return nil, fmt.Errorf("comment type %d cannot be used to create an auto merge comment", typ)
  1190  	}
  1191  	if err = pr.LoadIssue(ctx); err != nil {
  1192  		return nil, err
  1193  	}
  1194  
  1195  	if err = pr.LoadBaseRepo(ctx); err != nil {
  1196  		return nil, err
  1197  	}
  1198  
  1199  	comment, err = CreateComment(ctx, &CreateCommentOptions{
  1200  		Type:  typ,
  1201  		Doer:  doer,
  1202  		Repo:  pr.BaseRepo,
  1203  		Issue: pr.Issue,
  1204  	})
  1205  	return comment, err
  1206  }
  1207  
  1208  // RemapExternalUser ExternalUserRemappable interface
  1209  func (c *Comment) RemapExternalUser(externalName string, externalID, userID int64) error {
  1210  	c.OriginalAuthor = externalName
  1211  	c.OriginalAuthorID = externalID
  1212  	c.PosterID = userID
  1213  	return nil
  1214  }
  1215  
  1216  // GetUserID ExternalUserRemappable interface
  1217  func (c *Comment) GetUserID() int64 { return c.PosterID }
  1218  
  1219  // GetExternalName ExternalUserRemappable interface
  1220  func (c *Comment) GetExternalName() string { return c.OriginalAuthor }
  1221  
  1222  // GetExternalID ExternalUserRemappable interface
  1223  func (c *Comment) GetExternalID() int64 { return c.OriginalAuthorID }
  1224  
  1225  // CountCommentTypeLabelWithEmptyLabel count label comments with empty label
  1226  func CountCommentTypeLabelWithEmptyLabel(ctx context.Context) (int64, error) {
  1227  	return db.GetEngine(ctx).Where(builder.Eq{"type": CommentTypeLabel, "label_id": 0}).Count(new(Comment))
  1228  }
  1229  
  1230  // FixCommentTypeLabelWithEmptyLabel count label comments with empty label
  1231  func FixCommentTypeLabelWithEmptyLabel(ctx context.Context) (int64, error) {
  1232  	return db.GetEngine(ctx).Where(builder.Eq{"type": CommentTypeLabel, "label_id": 0}).Delete(new(Comment))
  1233  }
  1234  
  1235  // CountCommentTypeLabelWithOutsideLabels count label comments with outside label
  1236  func CountCommentTypeLabelWithOutsideLabels(ctx context.Context) (int64, error) {
  1237  	return db.GetEngine(ctx).Where("comment.type = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id))", CommentTypeLabel).
  1238  		Table("comment").
  1239  		Join("inner", "label", "label.id = comment.label_id").
  1240  		Join("inner", "issue", "issue.id = comment.issue_id ").
  1241  		Join("inner", "repository", "issue.repo_id = repository.id").
  1242  		Count()
  1243  }
  1244  
  1245  // FixCommentTypeLabelWithOutsideLabels count label comments with outside label
  1246  func FixCommentTypeLabelWithOutsideLabels(ctx context.Context) (int64, error) {
  1247  	res, err := db.GetEngine(ctx).Exec(`DELETE FROM comment WHERE comment.id IN (
  1248  		SELECT il_too.id FROM (
  1249  			SELECT com.id
  1250  				FROM comment AS com
  1251  					INNER JOIN label ON com.label_id = label.id
  1252  					INNER JOIN issue on issue.id = com.issue_id
  1253  					INNER JOIN repository ON issue.repo_id = repository.id
  1254  				WHERE
  1255  					com.type = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id))
  1256  	) AS il_too)`, CommentTypeLabel)
  1257  	if err != nil {
  1258  		return 0, err
  1259  	}
  1260  
  1261  	return res.RowsAffected()
  1262  }
  1263  
  1264  // HasOriginalAuthor returns if a comment was migrated and has an original author.
  1265  func (c *Comment) HasOriginalAuthor() bool {
  1266  	return c.OriginalAuthor != "" && c.OriginalAuthorID != 0
  1267  }
  1268  
  1269  // InsertIssueComments inserts many comments of issues.
  1270  func InsertIssueComments(ctx context.Context, comments []*Comment) error {
  1271  	if len(comments) == 0 {
  1272  		return nil
  1273  	}
  1274  
  1275  	issueIDs := container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
  1276  		return comment.IssueID, true
  1277  	})
  1278  
  1279  	ctx, committer, err := db.TxContext(ctx)
  1280  	if err != nil {
  1281  		return err
  1282  	}
  1283  	defer committer.Close()
  1284  	for _, comment := range comments {
  1285  		if _, err := db.GetEngine(ctx).NoAutoTime().Insert(comment); err != nil {
  1286  			return err
  1287  		}
  1288  
  1289  		for _, reaction := range comment.Reactions {
  1290  			reaction.IssueID = comment.IssueID
  1291  			reaction.CommentID = comment.ID
  1292  		}
  1293  		if len(comment.Reactions) > 0 {
  1294  			if err := db.Insert(ctx, comment.Reactions); err != nil {
  1295  				return err
  1296  			}
  1297  		}
  1298  	}
  1299  
  1300  	for _, issueID := range issueIDs {
  1301  		if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?",
  1302  			issueID, CommentTypeComment, issueID); err != nil {
  1303  			return err
  1304  		}
  1305  	}
  1306  	return committer.Commit()
  1307  }