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