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

     1  // Copyright 2014 The Gogs Authors. All rights reserved.
     2  // Copyright 2020 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package issues
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"html/template"
    11  	"regexp"
    12  	"slices"
    13  
    14  	"code.gitea.io/gitea/models/db"
    15  	project_model "code.gitea.io/gitea/models/project"
    16  	repo_model "code.gitea.io/gitea/models/repo"
    17  	user_model "code.gitea.io/gitea/models/user"
    18  	"code.gitea.io/gitea/modules/container"
    19  	"code.gitea.io/gitea/modules/log"
    20  	"code.gitea.io/gitea/modules/setting"
    21  	api "code.gitea.io/gitea/modules/structs"
    22  	"code.gitea.io/gitea/modules/timeutil"
    23  	"code.gitea.io/gitea/modules/util"
    24  
    25  	"xorm.io/builder"
    26  )
    27  
    28  // ErrIssueNotExist represents a "IssueNotExist" kind of error.
    29  type ErrIssueNotExist struct {
    30  	ID     int64
    31  	RepoID int64
    32  	Index  int64
    33  }
    34  
    35  // IsErrIssueNotExist checks if an error is a ErrIssueNotExist.
    36  func IsErrIssueNotExist(err error) bool {
    37  	_, ok := err.(ErrIssueNotExist)
    38  	return ok
    39  }
    40  
    41  func (err ErrIssueNotExist) Error() string {
    42  	return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index)
    43  }
    44  
    45  func (err ErrIssueNotExist) Unwrap() error {
    46  	return util.ErrNotExist
    47  }
    48  
    49  // ErrIssueIsClosed represents a "IssueIsClosed" kind of error.
    50  type ErrIssueIsClosed struct {
    51  	ID     int64
    52  	RepoID int64
    53  	Index  int64
    54  }
    55  
    56  // IsErrIssueIsClosed checks if an error is a ErrIssueNotExist.
    57  func IsErrIssueIsClosed(err error) bool {
    58  	_, ok := err.(ErrIssueIsClosed)
    59  	return ok
    60  }
    61  
    62  func (err ErrIssueIsClosed) Error() string {
    63  	return fmt.Sprintf("issue is closed [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index)
    64  }
    65  
    66  // ErrNewIssueInsert is used when the INSERT statement in newIssue fails
    67  type ErrNewIssueInsert struct {
    68  	OriginalError error
    69  }
    70  
    71  // IsErrNewIssueInsert checks if an error is a ErrNewIssueInsert.
    72  func IsErrNewIssueInsert(err error) bool {
    73  	_, ok := err.(ErrNewIssueInsert)
    74  	return ok
    75  }
    76  
    77  func (err ErrNewIssueInsert) Error() string {
    78  	return err.OriginalError.Error()
    79  }
    80  
    81  // ErrIssueWasClosed is used when close a closed issue
    82  type ErrIssueWasClosed struct {
    83  	ID    int64
    84  	Index int64
    85  }
    86  
    87  // IsErrIssueWasClosed checks if an error is a ErrIssueWasClosed.
    88  func IsErrIssueWasClosed(err error) bool {
    89  	_, ok := err.(ErrIssueWasClosed)
    90  	return ok
    91  }
    92  
    93  func (err ErrIssueWasClosed) Error() string {
    94  	return fmt.Sprintf("Issue [%d] %d was already closed", err.ID, err.Index)
    95  }
    96  
    97  // Issue represents an issue or pull request of repository.
    98  type Issue struct {
    99  	ID               int64                  `xorm:"pk autoincr"`
   100  	RepoID           int64                  `xorm:"INDEX UNIQUE(repo_index)"`
   101  	Repo             *repo_model.Repository `xorm:"-"`
   102  	Index            int64                  `xorm:"UNIQUE(repo_index)"` // Index in one repository.
   103  	PosterID         int64                  `xorm:"INDEX"`
   104  	Poster           *user_model.User       `xorm:"-"`
   105  	OriginalAuthor   string
   106  	OriginalAuthorID int64                  `xorm:"index"`
   107  	Title            string                 `xorm:"name"`
   108  	Content          string                 `xorm:"LONGTEXT"`
   109  	RenderedContent  template.HTML          `xorm:"-"`
   110  	Labels           []*Label               `xorm:"-"`
   111  	MilestoneID      int64                  `xorm:"INDEX"`
   112  	Milestone        *Milestone             `xorm:"-"`
   113  	Project          *project_model.Project `xorm:"-"`
   114  	Priority         int
   115  	AssigneeID       int64            `xorm:"-"`
   116  	Assignee         *user_model.User `xorm:"-"`
   117  	IsClosed         bool             `xorm:"INDEX"`
   118  	IsRead           bool             `xorm:"-"`
   119  	IsPull           bool             `xorm:"INDEX"` // Indicates whether is a pull request or not.
   120  	PullRequest      *PullRequest     `xorm:"-"`
   121  	NumComments      int
   122  	Ref              string
   123  	PinOrder         int `xorm:"DEFAULT 0"`
   124  
   125  	DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`
   126  
   127  	CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
   128  	UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
   129  	ClosedUnix  timeutil.TimeStamp `xorm:"INDEX"`
   130  
   131  	Attachments      []*repo_model.Attachment `xorm:"-"`
   132  	Comments         CommentList              `xorm:"-"`
   133  	Reactions        ReactionList             `xorm:"-"`
   134  	TotalTrackedTime int64                    `xorm:"-"`
   135  	Assignees        []*user_model.User       `xorm:"-"`
   136  
   137  	// IsLocked limits commenting abilities to users on an issue
   138  	// with write access
   139  	IsLocked bool `xorm:"NOT NULL DEFAULT false"`
   140  
   141  	// For view issue page.
   142  	ShowRole RoleDescriptor `xorm:"-"`
   143  }
   144  
   145  var (
   146  	issueTasksPat     = regexp.MustCompile(`(^\s*[-*]\s\[[\sxX]\]\s.)|(\n\s*[-*]\s\[[\sxX]\]\s.)`)
   147  	issueTasksDonePat = regexp.MustCompile(`(^\s*[-*]\s\[[xX]\]\s.)|(\n\s*[-*]\s\[[xX]\]\s.)`)
   148  )
   149  
   150  // IssueIndex represents the issue index table
   151  type IssueIndex db.ResourceIndex
   152  
   153  func init() {
   154  	db.RegisterModel(new(Issue))
   155  	db.RegisterModel(new(IssueIndex))
   156  }
   157  
   158  // LoadTotalTimes load total tracked time
   159  func (issue *Issue) LoadTotalTimes(ctx context.Context) (err error) {
   160  	opts := FindTrackedTimesOptions{IssueID: issue.ID}
   161  	issue.TotalTrackedTime, err = opts.toSession(db.GetEngine(ctx)).SumInt(&TrackedTime{}, "time")
   162  	if err != nil {
   163  		return err
   164  	}
   165  	return nil
   166  }
   167  
   168  // IsOverdue checks if the issue is overdue
   169  func (issue *Issue) IsOverdue() bool {
   170  	if issue.IsClosed {
   171  		return issue.ClosedUnix >= issue.DeadlineUnix
   172  	}
   173  	return timeutil.TimeStampNow() >= issue.DeadlineUnix
   174  }
   175  
   176  // LoadRepo loads issue's repository
   177  func (issue *Issue) LoadRepo(ctx context.Context) (err error) {
   178  	if issue.Repo == nil && issue.RepoID != 0 {
   179  		issue.Repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID)
   180  		if err != nil {
   181  			return fmt.Errorf("getRepositoryByID [%d]: %w", issue.RepoID, err)
   182  		}
   183  	}
   184  	return nil
   185  }
   186  
   187  // IsTimetrackerEnabled returns true if the repo enables timetracking
   188  func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
   189  	if err := issue.LoadRepo(ctx); err != nil {
   190  		log.Error(fmt.Sprintf("loadRepo: %v", err))
   191  		return false
   192  	}
   193  	return issue.Repo.IsTimetrackerEnabled(ctx)
   194  }
   195  
   196  // LoadPoster loads poster
   197  func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
   198  	if issue.Poster == nil && issue.PosterID != 0 {
   199  		issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID)
   200  		if err != nil {
   201  			issue.PosterID = user_model.GhostUserID
   202  			issue.Poster = user_model.NewGhostUser()
   203  			if !user_model.IsErrUserNotExist(err) {
   204  				return fmt.Errorf("getUserByID.(poster) [%d]: %w", issue.PosterID, err)
   205  			}
   206  			return nil
   207  		}
   208  	}
   209  	return err
   210  }
   211  
   212  // LoadPullRequest loads pull request info
   213  func (issue *Issue) LoadPullRequest(ctx context.Context) (err error) {
   214  	if issue.IsPull {
   215  		if issue.PullRequest == nil && issue.ID != 0 {
   216  			issue.PullRequest, err = GetPullRequestByIssueID(ctx, issue.ID)
   217  			if err != nil {
   218  				if IsErrPullRequestNotExist(err) {
   219  					return err
   220  				}
   221  				return fmt.Errorf("getPullRequestByIssueID [%d]: %w", issue.ID, err)
   222  			}
   223  		}
   224  		if issue.PullRequest != nil {
   225  			issue.PullRequest.Issue = issue
   226  		}
   227  	}
   228  	return nil
   229  }
   230  
   231  func (issue *Issue) loadComments(ctx context.Context) (err error) {
   232  	return issue.loadCommentsByType(ctx, CommentTypeUndefined)
   233  }
   234  
   235  // LoadDiscussComments loads discuss comments
   236  func (issue *Issue) LoadDiscussComments(ctx context.Context) error {
   237  	return issue.loadCommentsByType(ctx, CommentTypeComment)
   238  }
   239  
   240  func (issue *Issue) loadCommentsByType(ctx context.Context, tp CommentType) (err error) {
   241  	if issue.Comments != nil {
   242  		return nil
   243  	}
   244  	issue.Comments, err = FindComments(ctx, &FindCommentsOptions{
   245  		IssueID: issue.ID,
   246  		Type:    tp,
   247  	})
   248  	return err
   249  }
   250  
   251  func (issue *Issue) loadReactions(ctx context.Context) (err error) {
   252  	if issue.Reactions != nil {
   253  		return nil
   254  	}
   255  	reactions, _, err := FindReactions(ctx, FindReactionsOptions{
   256  		IssueID: issue.ID,
   257  	})
   258  	if err != nil {
   259  		return err
   260  	}
   261  	if err = issue.LoadRepo(ctx); err != nil {
   262  		return err
   263  	}
   264  	// Load reaction user data
   265  	if _, err := reactions.LoadUsers(ctx, issue.Repo); err != nil {
   266  		return err
   267  	}
   268  
   269  	// Cache comments to map
   270  	comments := make(map[int64]*Comment)
   271  	for _, comment := range issue.Comments {
   272  		comments[comment.ID] = comment
   273  	}
   274  	// Add reactions either to issue or comment
   275  	for _, react := range reactions {
   276  		if react.CommentID == 0 {
   277  			issue.Reactions = append(issue.Reactions, react)
   278  		} else if comment, ok := comments[react.CommentID]; ok {
   279  			comment.Reactions = append(comment.Reactions, react)
   280  		}
   281  	}
   282  	return nil
   283  }
   284  
   285  // LoadMilestone load milestone of this issue.
   286  func (issue *Issue) LoadMilestone(ctx context.Context) (err error) {
   287  	if (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {
   288  		issue.Milestone, err = GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID)
   289  		if err != nil && !IsErrMilestoneNotExist(err) {
   290  			return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %w", issue.RepoID, issue.MilestoneID, err)
   291  		}
   292  	}
   293  	return nil
   294  }
   295  
   296  // LoadAttributes loads the attribute of this issue.
   297  func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
   298  	if err = issue.LoadRepo(ctx); err != nil {
   299  		return err
   300  	}
   301  
   302  	if err = issue.LoadPoster(ctx); err != nil {
   303  		return err
   304  	}
   305  
   306  	if err = issue.LoadLabels(ctx); err != nil {
   307  		return err
   308  	}
   309  
   310  	if err = issue.LoadMilestone(ctx); err != nil {
   311  		return err
   312  	}
   313  
   314  	if err = issue.LoadProject(ctx); err != nil {
   315  		return err
   316  	}
   317  
   318  	if err = issue.LoadAssignees(ctx); err != nil {
   319  		return err
   320  	}
   321  
   322  	if err = issue.LoadPullRequest(ctx); err != nil && !IsErrPullRequestNotExist(err) {
   323  		// It is possible pull request is not yet created.
   324  		return err
   325  	}
   326  
   327  	if issue.Attachments == nil {
   328  		issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)
   329  		if err != nil {
   330  			return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)
   331  		}
   332  	}
   333  
   334  	if err = issue.loadComments(ctx); err != nil {
   335  		return err
   336  	}
   337  
   338  	if err = issue.Comments.LoadAttributes(ctx); err != nil {
   339  		return err
   340  	}
   341  	if issue.IsTimetrackerEnabled(ctx) {
   342  		if err = issue.LoadTotalTimes(ctx); err != nil {
   343  			return err
   344  		}
   345  	}
   346  
   347  	return issue.loadReactions(ctx)
   348  }
   349  
   350  // GetIsRead load the `IsRead` field of the issue
   351  func (issue *Issue) GetIsRead(ctx context.Context, userID int64) error {
   352  	issueUser := &IssueUser{IssueID: issue.ID, UID: userID}
   353  	if has, err := db.GetEngine(ctx).Get(issueUser); err != nil {
   354  		return err
   355  	} else if !has {
   356  		issue.IsRead = false
   357  		return nil
   358  	}
   359  	issue.IsRead = issueUser.IsRead
   360  	return nil
   361  }
   362  
   363  // APIURL returns the absolute APIURL to this issue.
   364  func (issue *Issue) APIURL(ctx context.Context) string {
   365  	if issue.Repo == nil {
   366  		err := issue.LoadRepo(ctx)
   367  		if err != nil {
   368  			log.Error("Issue[%d].APIURL(): %v", issue.ID, err)
   369  			return ""
   370  		}
   371  	}
   372  	return fmt.Sprintf("%s/issues/%d", issue.Repo.APIURL(), issue.Index)
   373  }
   374  
   375  // HTMLURL returns the absolute URL to this issue.
   376  func (issue *Issue) HTMLURL() string {
   377  	var path string
   378  	if issue.IsPull {
   379  		path = "pulls"
   380  	} else {
   381  		path = "issues"
   382  	}
   383  	return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(), path, issue.Index)
   384  }
   385  
   386  // Link returns the issue's relative URL.
   387  func (issue *Issue) Link() string {
   388  	var path string
   389  	if issue.IsPull {
   390  		path = "pulls"
   391  	} else {
   392  		path = "issues"
   393  	}
   394  	return fmt.Sprintf("%s/%s/%d", issue.Repo.Link(), path, issue.Index)
   395  }
   396  
   397  // DiffURL returns the absolute URL to this diff
   398  func (issue *Issue) DiffURL() string {
   399  	if issue.IsPull {
   400  		return fmt.Sprintf("%s/pulls/%d.diff", issue.Repo.HTMLURL(), issue.Index)
   401  	}
   402  	return ""
   403  }
   404  
   405  // PatchURL returns the absolute URL to this patch
   406  func (issue *Issue) PatchURL() string {
   407  	if issue.IsPull {
   408  		return fmt.Sprintf("%s/pulls/%d.patch", issue.Repo.HTMLURL(), issue.Index)
   409  	}
   410  	return ""
   411  }
   412  
   413  // State returns string representation of issue status.
   414  func (issue *Issue) State() api.StateType {
   415  	if issue.IsClosed {
   416  		return api.StateClosed
   417  	}
   418  	return api.StateOpen
   419  }
   420  
   421  // HashTag returns unique hash tag for issue.
   422  func (issue *Issue) HashTag() string {
   423  	return fmt.Sprintf("issue-%d", issue.ID)
   424  }
   425  
   426  // IsPoster returns true if given user by ID is the poster.
   427  func (issue *Issue) IsPoster(uid int64) bool {
   428  	return issue.OriginalAuthorID == 0 && issue.PosterID == uid
   429  }
   430  
   431  // GetTasks returns the amount of tasks in the issues content
   432  func (issue *Issue) GetTasks() int {
   433  	return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
   434  }
   435  
   436  // GetTasksDone returns the amount of completed tasks in the issues content
   437  func (issue *Issue) GetTasksDone() int {
   438  	return len(issueTasksDonePat.FindAllStringIndex(issue.Content, -1))
   439  }
   440  
   441  // GetLastEventTimestamp returns the last user visible event timestamp, either the creation of this issue or the close.
   442  func (issue *Issue) GetLastEventTimestamp() timeutil.TimeStamp {
   443  	if issue.IsClosed {
   444  		return issue.ClosedUnix
   445  	}
   446  	return issue.CreatedUnix
   447  }
   448  
   449  // GetLastEventLabel returns the localization label for the current issue.
   450  func (issue *Issue) GetLastEventLabel() string {
   451  	if issue.IsClosed {
   452  		if issue.IsPull && issue.PullRequest.HasMerged {
   453  			return "repo.pulls.merged_by"
   454  		}
   455  		return "repo.issues.closed_by"
   456  	}
   457  	return "repo.issues.opened_by"
   458  }
   459  
   460  // GetLastComment return last comment for the current issue.
   461  func (issue *Issue) GetLastComment(ctx context.Context) (*Comment, error) {
   462  	var c Comment
   463  	exist, err := db.GetEngine(ctx).Where("type = ?", CommentTypeComment).
   464  		And("issue_id = ?", issue.ID).Desc("created_unix").Get(&c)
   465  	if err != nil {
   466  		return nil, err
   467  	}
   468  	if !exist {
   469  		return nil, nil
   470  	}
   471  	return &c, nil
   472  }
   473  
   474  // GetLastEventLabelFake returns the localization label for the current issue without providing a link in the username.
   475  func (issue *Issue) GetLastEventLabelFake() string {
   476  	if issue.IsClosed {
   477  		if issue.IsPull && issue.PullRequest.HasMerged {
   478  			return "repo.pulls.merged_by_fake"
   479  		}
   480  		return "repo.issues.closed_by_fake"
   481  	}
   482  	return "repo.issues.opened_by_fake"
   483  }
   484  
   485  // GetIssueByIndex returns raw issue without loading attributes by index in a repository.
   486  func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
   487  	if index < 1 {
   488  		return nil, ErrIssueNotExist{}
   489  	}
   490  	issue := &Issue{
   491  		RepoID: repoID,
   492  		Index:  index,
   493  	}
   494  	has, err := db.GetEngine(ctx).Get(issue)
   495  	if err != nil {
   496  		return nil, err
   497  	} else if !has {
   498  		return nil, ErrIssueNotExist{0, repoID, index}
   499  	}
   500  	return issue, nil
   501  }
   502  
   503  // GetIssueWithAttrsByIndex returns issue by index in a repository.
   504  func GetIssueWithAttrsByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
   505  	issue, err := GetIssueByIndex(ctx, repoID, index)
   506  	if err != nil {
   507  		return nil, err
   508  	}
   509  	return issue, issue.LoadAttributes(ctx)
   510  }
   511  
   512  // GetIssueByID returns an issue by given ID.
   513  func GetIssueByID(ctx context.Context, id int64) (*Issue, error) {
   514  	issue := new(Issue)
   515  	has, err := db.GetEngine(ctx).ID(id).Get(issue)
   516  	if err != nil {
   517  		return nil, err
   518  	} else if !has {
   519  		return nil, ErrIssueNotExist{id, 0, 0}
   520  	}
   521  	return issue, nil
   522  }
   523  
   524  // GetIssuesByIDs return issues with the given IDs.
   525  // If keepOrder is true, the order of the returned issues will be the same as the given IDs.
   526  func GetIssuesByIDs(ctx context.Context, issueIDs []int64, keepOrder ...bool) (IssueList, error) {
   527  	issues := make([]*Issue, 0, len(issueIDs))
   528  
   529  	if err := db.GetEngine(ctx).In("id", issueIDs).Find(&issues); err != nil {
   530  		return nil, err
   531  	}
   532  
   533  	if len(keepOrder) > 0 && keepOrder[0] {
   534  		m := make(map[int64]*Issue, len(issues))
   535  		appended := container.Set[int64]{}
   536  		for _, issue := range issues {
   537  			m[issue.ID] = issue
   538  		}
   539  		issues = issues[:0]
   540  		for _, id := range issueIDs {
   541  			if issue, ok := m[id]; ok && !appended.Contains(id) { // make sure the id is existed and not appended
   542  				appended.Add(id)
   543  				issues = append(issues, issue)
   544  			}
   545  		}
   546  	}
   547  
   548  	return issues, nil
   549  }
   550  
   551  // GetIssueIDsByRepoID returns all issue ids by repo id
   552  func GetIssueIDsByRepoID(ctx context.Context, repoID int64) ([]int64, error) {
   553  	ids := make([]int64, 0, 10)
   554  	err := db.GetEngine(ctx).Table("issue").Cols("id").Where("repo_id = ?", repoID).Find(&ids)
   555  	return ids, err
   556  }
   557  
   558  // GetParticipantsIDsByIssueID returns the IDs of all users who participated in comments of an issue,
   559  // but skips joining with `user` for performance reasons.
   560  // User permissions must be verified elsewhere if required.
   561  func GetParticipantsIDsByIssueID(ctx context.Context, issueID int64) ([]int64, error) {
   562  	userIDs := make([]int64, 0, 5)
   563  	return userIDs, db.GetEngine(ctx).
   564  		Table("comment").
   565  		Cols("poster_id").
   566  		Where("issue_id = ?", issueID).
   567  		And("type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview).
   568  		Distinct("poster_id").
   569  		Find(&userIDs)
   570  }
   571  
   572  // IsUserParticipantsOfIssue return true if user is participants of an issue
   573  func IsUserParticipantsOfIssue(ctx context.Context, user *user_model.User, issue *Issue) bool {
   574  	userIDs, err := issue.GetParticipantIDsByIssue(ctx)
   575  	if err != nil {
   576  		log.Error(err.Error())
   577  		return false
   578  	}
   579  	return slices.Contains(userIDs, user.ID)
   580  }
   581  
   582  // DependencyInfo represents high level information about an issue which is a dependency of another issue.
   583  type DependencyInfo struct {
   584  	Issue                 `xorm:"extends"`
   585  	repo_model.Repository `xorm:"extends"`
   586  }
   587  
   588  // GetParticipantIDsByIssue returns all userIDs who are participated in comments of an issue and issue author
   589  func (issue *Issue) GetParticipantIDsByIssue(ctx context.Context) ([]int64, error) {
   590  	if issue == nil {
   591  		return nil, nil
   592  	}
   593  	userIDs := make([]int64, 0, 5)
   594  	if err := db.GetEngine(ctx).Table("comment").Cols("poster_id").
   595  		Where("`comment`.issue_id = ?", issue.ID).
   596  		And("`comment`.type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview).
   597  		And("`user`.is_active = ?", true).
   598  		And("`user`.prohibit_login = ?", false).
   599  		Join("INNER", "`user`", "`user`.id = `comment`.poster_id").
   600  		Distinct("poster_id").
   601  		Find(&userIDs); err != nil {
   602  		return nil, fmt.Errorf("get poster IDs: %w", err)
   603  	}
   604  	if !slices.Contains(userIDs, issue.PosterID) {
   605  		return append(userIDs, issue.PosterID), nil
   606  	}
   607  	return userIDs, nil
   608  }
   609  
   610  // BlockedByDependencies finds all Dependencies an issue is blocked by
   611  func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptions) (issueDeps []*DependencyInfo, err error) {
   612  	sess := db.GetEngine(ctx).
   613  		Table("issue").
   614  		Join("INNER", "repository", "repository.id = issue.repo_id").
   615  		Join("INNER", "issue_dependency", "issue_dependency.dependency_id = issue.id").
   616  		Where("issue_id = ?", issue.ID).
   617  		// sort by repo id then created date, with the issues of the same repo at the beginning of the list
   618  		OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID)
   619  	if opts.Page != 0 {
   620  		sess = db.SetSessionPagination(sess, &opts)
   621  	}
   622  	err = sess.Find(&issueDeps)
   623  
   624  	for _, depInfo := range issueDeps {
   625  		depInfo.Issue.Repo = &depInfo.Repository
   626  	}
   627  
   628  	return issueDeps, err
   629  }
   630  
   631  // BlockingDependencies returns all blocking dependencies, aka all other issues a given issue blocks
   632  func (issue *Issue) BlockingDependencies(ctx context.Context) (issueDeps []*DependencyInfo, err error) {
   633  	err = db.GetEngine(ctx).
   634  		Table("issue").
   635  		Join("INNER", "repository", "repository.id = issue.repo_id").
   636  		Join("INNER", "issue_dependency", "issue_dependency.issue_id = issue.id").
   637  		Where("dependency_id = ?", issue.ID).
   638  		// sort by repo id then created date, with the issues of the same repo at the beginning of the list
   639  		OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID).
   640  		Find(&issueDeps)
   641  
   642  	for _, depInfo := range issueDeps {
   643  		depInfo.Issue.Repo = &depInfo.Repository
   644  	}
   645  
   646  	return issueDeps, err
   647  }
   648  
   649  func migratedIssueCond(tp api.GitServiceType) builder.Cond {
   650  	return builder.In("issue_id",
   651  		builder.Select("issue.id").
   652  			From("issue").
   653  			InnerJoin("repository", "issue.repo_id = repository.id").
   654  			Where(builder.Eq{
   655  				"repository.original_service_type": tp,
   656  			}),
   657  	)
   658  }
   659  
   660  // RemapExternalUser ExternalUserRemappable interface
   661  func (issue *Issue) RemapExternalUser(externalName string, externalID, userID int64) error {
   662  	issue.OriginalAuthor = externalName
   663  	issue.OriginalAuthorID = externalID
   664  	issue.PosterID = userID
   665  	return nil
   666  }
   667  
   668  // GetUserID ExternalUserRemappable interface
   669  func (issue *Issue) GetUserID() int64 { return issue.PosterID }
   670  
   671  // GetExternalName ExternalUserRemappable interface
   672  func (issue *Issue) GetExternalName() string { return issue.OriginalAuthor }
   673  
   674  // GetExternalID ExternalUserRemappable interface
   675  func (issue *Issue) GetExternalID() int64 { return issue.OriginalAuthorID }
   676  
   677  // HasOriginalAuthor returns if an issue was migrated and has an original author.
   678  func (issue *Issue) HasOriginalAuthor() bool {
   679  	return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0
   680  }
   681  
   682  var ErrIssueMaxPinReached = util.NewInvalidArgumentErrorf("the max number of pinned issues has been readched")
   683  
   684  // IsPinned returns if a Issue is pinned
   685  func (issue *Issue) IsPinned() bool {
   686  	return issue.PinOrder != 0
   687  }
   688  
   689  // Pin pins a Issue
   690  func (issue *Issue) Pin(ctx context.Context, user *user_model.User) error {
   691  	// If the Issue is already pinned, we don't need to pin it twice
   692  	if issue.IsPinned() {
   693  		return nil
   694  	}
   695  
   696  	var maxPin int
   697  	_, err := db.GetEngine(ctx).SQL("SELECT MAX(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ?", issue.RepoID, issue.IsPull).Get(&maxPin)
   698  	if err != nil {
   699  		return err
   700  	}
   701  
   702  	// Check if the maximum allowed Pins reached
   703  	if maxPin >= setting.Repository.Issue.MaxPinned {
   704  		return ErrIssueMaxPinReached
   705  	}
   706  
   707  	_, err = db.GetEngine(ctx).Table("issue").
   708  		Where("id = ?", issue.ID).
   709  		Update(map[string]any{
   710  			"pin_order": maxPin + 1,
   711  		})
   712  	if err != nil {
   713  		return err
   714  	}
   715  
   716  	// Add the pin event to the history
   717  	opts := &CreateCommentOptions{
   718  		Type:  CommentTypePin,
   719  		Doer:  user,
   720  		Repo:  issue.Repo,
   721  		Issue: issue,
   722  	}
   723  	if _, err = CreateComment(ctx, opts); err != nil {
   724  		return err
   725  	}
   726  
   727  	return nil
   728  }
   729  
   730  // UnpinIssue unpins a Issue
   731  func (issue *Issue) Unpin(ctx context.Context, user *user_model.User) error {
   732  	// If the Issue is not pinned, we don't need to unpin it
   733  	if !issue.IsPinned() {
   734  		return nil
   735  	}
   736  
   737  	// This sets the Pin for all Issues that come after the unpined Issue to the correct value
   738  	_, err := db.GetEngine(ctx).Exec("UPDATE issue SET pin_order = pin_order - 1 WHERE repo_id = ? AND is_pull = ? AND pin_order > ?", issue.RepoID, issue.IsPull, issue.PinOrder)
   739  	if err != nil {
   740  		return err
   741  	}
   742  
   743  	_, err = db.GetEngine(ctx).Table("issue").
   744  		Where("id = ?", issue.ID).
   745  		Update(map[string]any{
   746  			"pin_order": 0,
   747  		})
   748  	if err != nil {
   749  		return err
   750  	}
   751  
   752  	// Add the unpin event to the history
   753  	opts := &CreateCommentOptions{
   754  		Type:  CommentTypeUnpin,
   755  		Doer:  user,
   756  		Repo:  issue.Repo,
   757  		Issue: issue,
   758  	}
   759  	if _, err = CreateComment(ctx, opts); err != nil {
   760  		return err
   761  	}
   762  
   763  	return nil
   764  }
   765  
   766  // PinOrUnpin pins or unpins a Issue
   767  func (issue *Issue) PinOrUnpin(ctx context.Context, user *user_model.User) error {
   768  	if !issue.IsPinned() {
   769  		return issue.Pin(ctx, user)
   770  	}
   771  
   772  	return issue.Unpin(ctx, user)
   773  }
   774  
   775  // MovePin moves a Pinned Issue to a new Position
   776  func (issue *Issue) MovePin(ctx context.Context, newPosition int) error {
   777  	// If the Issue is not pinned, we can't move them
   778  	if !issue.IsPinned() {
   779  		return nil
   780  	}
   781  
   782  	if newPosition < 1 {
   783  		return fmt.Errorf("The Position can't be lower than 1")
   784  	}
   785  
   786  	dbctx, committer, err := db.TxContext(ctx)
   787  	if err != nil {
   788  		return err
   789  	}
   790  	defer committer.Close()
   791  
   792  	var maxPin int
   793  	_, err = db.GetEngine(dbctx).SQL("SELECT MAX(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ?", issue.RepoID, issue.IsPull).Get(&maxPin)
   794  	if err != nil {
   795  		return err
   796  	}
   797  
   798  	// If the new Position bigger than the current Maximum, set it to the Maximum
   799  	if newPosition > maxPin+1 {
   800  		newPosition = maxPin + 1
   801  	}
   802  
   803  	// Lower the Position of all Pinned Issue that came after the current Position
   804  	_, err = db.GetEngine(dbctx).Exec("UPDATE issue SET pin_order = pin_order - 1 WHERE repo_id = ? AND is_pull = ? AND pin_order > ?", issue.RepoID, issue.IsPull, issue.PinOrder)
   805  	if err != nil {
   806  		return err
   807  	}
   808  
   809  	// Higher the Position of all Pinned Issues that comes after the new Position
   810  	_, err = db.GetEngine(dbctx).Exec("UPDATE issue SET pin_order = pin_order + 1 WHERE repo_id = ? AND is_pull = ? AND pin_order >= ?", issue.RepoID, issue.IsPull, newPosition)
   811  	if err != nil {
   812  		return err
   813  	}
   814  
   815  	_, err = db.GetEngine(dbctx).Table("issue").
   816  		Where("id = ?", issue.ID).
   817  		Update(map[string]any{
   818  			"pin_order": newPosition,
   819  		})
   820  	if err != nil {
   821  		return err
   822  	}
   823  
   824  	return committer.Commit()
   825  }
   826  
   827  // GetPinnedIssues returns the pinned Issues for the given Repo and type
   828  func GetPinnedIssues(ctx context.Context, repoID int64, isPull bool) (IssueList, error) {
   829  	issues := make(IssueList, 0)
   830  
   831  	err := db.GetEngine(ctx).
   832  		Table("issue").
   833  		Where("repo_id = ?", repoID).
   834  		And("is_pull = ?", isPull).
   835  		And("pin_order > 0").
   836  		OrderBy("pin_order").
   837  		Find(&issues)
   838  	if err != nil {
   839  		return nil, err
   840  	}
   841  
   842  	err = issues.LoadAttributes(ctx)
   843  	if err != nil {
   844  		return nil, err
   845  	}
   846  
   847  	return issues, nil
   848  }
   849  
   850  // IsNewPinnedAllowed returns if a new Issue or Pull request can be pinned
   851  func IsNewPinAllowed(ctx context.Context, repoID int64, isPull bool) (bool, error) {
   852  	var maxPin int
   853  	_, err := db.GetEngine(ctx).SQL("SELECT COUNT(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ? AND pin_order > 0", repoID, isPull).Get(&maxPin)
   854  	if err != nil {
   855  		return false, err
   856  	}
   857  
   858  	return maxPin < setting.Repository.Issue.MaxPinned, nil
   859  }
   860  
   861  // IsErrIssueMaxPinReached returns if the error is, that the User can't pin more Issues
   862  func IsErrIssueMaxPinReached(err error) bool {
   863  	return err == ErrIssueMaxPinReached
   864  }
   865  
   866  // InsertIssues insert issues to database
   867  func InsertIssues(ctx context.Context, issues ...*Issue) error {
   868  	ctx, committer, err := db.TxContext(ctx)
   869  	if err != nil {
   870  		return err
   871  	}
   872  	defer committer.Close()
   873  
   874  	for _, issue := range issues {
   875  		if err := insertIssue(ctx, issue); err != nil {
   876  			return err
   877  		}
   878  	}
   879  	return committer.Commit()
   880  }
   881  
   882  func insertIssue(ctx context.Context, issue *Issue) error {
   883  	sess := db.GetEngine(ctx)
   884  	if _, err := sess.NoAutoTime().Insert(issue); err != nil {
   885  		return err
   886  	}
   887  	issueLabels := make([]IssueLabel, 0, len(issue.Labels))
   888  	for _, label := range issue.Labels {
   889  		issueLabels = append(issueLabels, IssueLabel{
   890  			IssueID: issue.ID,
   891  			LabelID: label.ID,
   892  		})
   893  	}
   894  	if len(issueLabels) > 0 {
   895  		if _, err := sess.Insert(issueLabels); err != nil {
   896  			return err
   897  		}
   898  	}
   899  
   900  	for _, reaction := range issue.Reactions {
   901  		reaction.IssueID = issue.ID
   902  	}
   903  
   904  	if len(issue.Reactions) > 0 {
   905  		if _, err := sess.Insert(issue.Reactions); err != nil {
   906  			return err
   907  		}
   908  	}
   909  
   910  	return nil
   911  }