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

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package issue
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  
    10  	activities_model "code.gitea.io/gitea/models/activities"
    11  	"code.gitea.io/gitea/models/db"
    12  	issues_model "code.gitea.io/gitea/models/issues"
    13  	access_model "code.gitea.io/gitea/models/perm/access"
    14  	project_model "code.gitea.io/gitea/models/project"
    15  	repo_model "code.gitea.io/gitea/models/repo"
    16  	system_model "code.gitea.io/gitea/models/system"
    17  	user_model "code.gitea.io/gitea/models/user"
    18  	"code.gitea.io/gitea/modules/container"
    19  	"code.gitea.io/gitea/modules/git"
    20  	"code.gitea.io/gitea/modules/log"
    21  	"code.gitea.io/gitea/modules/storage"
    22  	notify_service "code.gitea.io/gitea/services/notify"
    23  )
    24  
    25  // NewIssue creates new issue with labels for repository.
    26  func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64, projectID int64) error {
    27  	if err := issue.LoadPoster(ctx); err != nil {
    28  		return err
    29  	}
    30  
    31  	if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) {
    32  		return user_model.ErrBlockedUser
    33  	}
    34  
    35  	if err := db.WithTx(ctx, func(ctx context.Context) error {
    36  		if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
    37  			return err
    38  		}
    39  		for _, assigneeID := range assigneeIDs {
    40  			if _, err := AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, true); err != nil {
    41  				return err
    42  			}
    43  		}
    44  		if projectID > 0 {
    45  			if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, issue.Poster, projectID, 0); err != nil {
    46  				return err
    47  			}
    48  		}
    49  		return nil
    50  	}); err != nil {
    51  		return err
    52  	}
    53  
    54  	mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content)
    55  	if err != nil {
    56  		return err
    57  	}
    58  
    59  	notify_service.NewIssue(ctx, issue, mentions)
    60  	if len(issue.Labels) > 0 {
    61  		notify_service.IssueChangeLabels(ctx, issue.Poster, issue, issue.Labels, nil)
    62  	}
    63  	if issue.Milestone != nil {
    64  		notify_service.IssueChangeMilestone(ctx, issue.Poster, issue, 0)
    65  	}
    66  
    67  	return nil
    68  }
    69  
    70  // ChangeTitle changes the title of this issue, as the given user.
    71  func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, title string) error {
    72  	oldTitle := issue.Title
    73  	issue.Title = title
    74  
    75  	if oldTitle == title {
    76  		return nil
    77  	}
    78  
    79  	if err := issue.LoadRepo(ctx); err != nil {
    80  		return err
    81  	}
    82  
    83  	if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
    84  		if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin {
    85  			return user_model.ErrBlockedUser
    86  		}
    87  	}
    88  
    89  	if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
    90  		return err
    91  	}
    92  
    93  	var reviewNotifiers []*ReviewRequestNotifier
    94  	if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) {
    95  		var err error
    96  		reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest)
    97  		if err != nil {
    98  			log.Error("PullRequestCodeOwnersReview: %v", err)
    99  		}
   100  	}
   101  
   102  	notify_service.IssueChangeTitle(ctx, doer, issue, oldTitle)
   103  	ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifiers)
   104  
   105  	return nil
   106  }
   107  
   108  // ChangeIssueRef changes the branch of this issue, as the given user.
   109  func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, ref string) error {
   110  	oldRef := issue.Ref
   111  	issue.Ref = ref
   112  
   113  	if err := issues_model.ChangeIssueRef(ctx, issue, doer, oldRef); err != nil {
   114  		return err
   115  	}
   116  
   117  	notify_service.IssueChangeRef(ctx, doer, issue, oldRef)
   118  
   119  	return nil
   120  }
   121  
   122  // UpdateAssignees is a helper function to add or delete one or multiple issue assignee(s)
   123  // Deleting is done the GitHub way (quote from their api documentation):
   124  // https://developer.github.com/v3/issues/#edit-an-issue
   125  // "assignees" (array): Logins for Users to assign to this issue.
   126  // Pass one or more user logins to replace the set of assignees on this Issue.
   127  // Send an empty array ([]) to clear all assignees from the Issue.
   128  func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) {
   129  	uniqueAssignees := container.SetOf(multipleAssignees...)
   130  
   131  	// Keep the old assignee thingy for compatibility reasons
   132  	if oneAssignee != "" {
   133  		uniqueAssignees.Add(oneAssignee)
   134  	}
   135  
   136  	// Loop through all assignees to add them
   137  	allNewAssignees := make([]*user_model.User, 0, len(uniqueAssignees))
   138  	for _, assigneeName := range uniqueAssignees.Values() {
   139  		assignee, err := user_model.GetUserByName(ctx, assigneeName)
   140  		if err != nil {
   141  			return err
   142  		}
   143  
   144  		if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) {
   145  			return user_model.ErrBlockedUser
   146  		}
   147  
   148  		allNewAssignees = append(allNewAssignees, assignee)
   149  	}
   150  
   151  	// Delete all old assignees not passed
   152  	if err = DeleteNotPassedAssignee(ctx, issue, doer, allNewAssignees); err != nil {
   153  		return err
   154  	}
   155  
   156  	// Add all new assignees
   157  	// Update the assignee. The function will check if the user exists, is already
   158  	// assigned (which he shouldn't as we deleted all assignees before) and
   159  	// has access to the repo.
   160  	for _, assignee := range allNewAssignees {
   161  		// Extra method to prevent double adding (which would result in removing)
   162  		_, err = AddAssigneeIfNotAssigned(ctx, issue, doer, assignee.ID, true)
   163  		if err != nil {
   164  			return err
   165  		}
   166  	}
   167  
   168  	return err
   169  }
   170  
   171  // DeleteIssue deletes an issue
   172  func DeleteIssue(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue) error {
   173  	// load issue before deleting it
   174  	if err := issue.LoadAttributes(ctx); err != nil {
   175  		return err
   176  	}
   177  	if err := issue.LoadPullRequest(ctx); err != nil {
   178  		return err
   179  	}
   180  
   181  	// delete entries in database
   182  	if err := deleteIssue(ctx, issue); err != nil {
   183  		return err
   184  	}
   185  
   186  	// delete pull request related git data
   187  	if issue.IsPull && gitRepo != nil {
   188  		if err := gitRepo.RemoveReference(fmt.Sprintf("%s%d/head", git.PullPrefix, issue.PullRequest.Index)); err != nil {
   189  			return err
   190  		}
   191  	}
   192  
   193  	// If the Issue is pinned, we should unpin it before deletion to avoid problems with other pinned Issues
   194  	if issue.IsPinned() {
   195  		if err := issue.Unpin(ctx, doer); err != nil {
   196  			return err
   197  		}
   198  	}
   199  
   200  	notify_service.DeleteIssue(ctx, doer, issue)
   201  
   202  	return nil
   203  }
   204  
   205  // AddAssigneeIfNotAssigned adds an assignee only if he isn't already assigned to the issue.
   206  // Also checks for access of assigned user
   207  func AddAssigneeIfNotAssigned(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assigneeID int64, notify bool) (comment *issues_model.Comment, err error) {
   208  	assignee, err := user_model.GetUserByID(ctx, assigneeID)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	// Check if the user is already assigned
   214  	isAssigned, err := issues_model.IsUserAssignedToIssue(ctx, issue, assignee)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  	if isAssigned {
   219  		// nothing to to
   220  		return nil, nil
   221  	}
   222  
   223  	valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull)
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  	if !valid {
   228  		return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name}
   229  	}
   230  
   231  	if notify {
   232  		_, comment, err = ToggleAssigneeWithNotify(ctx, issue, doer, assigneeID)
   233  		return comment, err
   234  	}
   235  	_, comment, err = issues_model.ToggleIssueAssignee(ctx, issue, doer, assigneeID)
   236  	return comment, err
   237  }
   238  
   239  // GetRefEndNamesAndURLs retrieves the ref end names (e.g. refs/heads/branch-name -> branch-name)
   240  // and their respective URLs.
   241  func GetRefEndNamesAndURLs(issues []*issues_model.Issue, repoLink string) (map[int64]string, map[int64]string) {
   242  	issueRefEndNames := make(map[int64]string, len(issues))
   243  	issueRefURLs := make(map[int64]string, len(issues))
   244  	for _, issue := range issues {
   245  		if issue.Ref != "" {
   246  			issueRefEndNames[issue.ID] = git.RefName(issue.Ref).ShortName()
   247  			issueRefURLs[issue.ID] = git.RefURL(repoLink, issue.Ref)
   248  		}
   249  	}
   250  	return issueRefEndNames, issueRefURLs
   251  }
   252  
   253  // deleteIssue deletes the issue
   254  func deleteIssue(ctx context.Context, issue *issues_model.Issue) error {
   255  	ctx, committer, err := db.TxContext(ctx)
   256  	if err != nil {
   257  		return err
   258  	}
   259  	defer committer.Close()
   260  
   261  	e := db.GetEngine(ctx)
   262  	if _, err := e.ID(issue.ID).NoAutoCondition().Delete(issue); err != nil {
   263  		return err
   264  	}
   265  
   266  	// update the total issue numbers
   267  	if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false); err != nil {
   268  		return err
   269  	}
   270  	// if the issue is closed, update the closed issue numbers
   271  	if issue.IsClosed {
   272  		if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil {
   273  			return err
   274  		}
   275  	}
   276  
   277  	if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
   278  		return fmt.Errorf("error updating counters for milestone id %d: %w",
   279  			issue.MilestoneID, err)
   280  	}
   281  
   282  	if err := activities_model.DeleteIssueActions(ctx, issue.RepoID, issue.ID, issue.Index); err != nil {
   283  		return err
   284  	}
   285  
   286  	// find attachments related to this issue and remove them
   287  	if err := issue.LoadAttributes(ctx); err != nil {
   288  		return err
   289  	}
   290  
   291  	for i := range issue.Attachments {
   292  		system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", issue.Attachments[i].RelativePath())
   293  	}
   294  
   295  	// delete all database data still assigned to this issue
   296  	if err := db.DeleteBeans(ctx,
   297  		&issues_model.ContentHistory{IssueID: issue.ID},
   298  		&issues_model.Comment{IssueID: issue.ID},
   299  		&issues_model.IssueLabel{IssueID: issue.ID},
   300  		&issues_model.IssueDependency{IssueID: issue.ID},
   301  		&issues_model.IssueAssignees{IssueID: issue.ID},
   302  		&issues_model.IssueUser{IssueID: issue.ID},
   303  		&activities_model.Notification{IssueID: issue.ID},
   304  		&issues_model.Reaction{IssueID: issue.ID},
   305  		&issues_model.IssueWatch{IssueID: issue.ID},
   306  		&issues_model.Stopwatch{IssueID: issue.ID},
   307  		&issues_model.TrackedTime{IssueID: issue.ID},
   308  		&project_model.ProjectIssue{IssueID: issue.ID},
   309  		&repo_model.Attachment{IssueID: issue.ID},
   310  		&issues_model.PullRequest{IssueID: issue.ID},
   311  		&issues_model.Comment{RefIssueID: issue.ID},
   312  		&issues_model.IssueDependency{DependencyID: issue.ID},
   313  		&issues_model.Comment{DependentIssueID: issue.ID},
   314  	); err != nil {
   315  		return err
   316  	}
   317  
   318  	return committer.Commit()
   319  }