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