// Copyright 2023 The Gitea Authors. All rights reserved.// SPDX-License-Identifier: MIT package issues import ( "context" "fmt" "strings" "forgejo.org/models/db" "forgejo.org/models/organization" "forgejo.org/models/perm" access_model "forgejo.org/models/perm/access" project_model "forgejo.org/models/project" repo_model "forgejo.org/models/repo" system_model "forgejo.org/models/system" "forgejo.org/models/unit" user_model "forgejo.org/models/user" "forgejo.org/modules/git" "forgejo.org/modules/references" api "forgejo.org/modules/structs" "forgejo.org/modules/timeutil" "forgejo.org/modules/util" "xorm.io/builder") func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error { _, err := UpdateIssueColsWithCond(ctx, issue, builder.NewCond(), cols...) return err} func UpdateIssueColsWithCond(ctx context.Context, issue *Issue, cond builder.Cond, cols ...string) (int64, error) { sess := db.GetEngine(ctx).ID(issue.ID) if issue.NoAutoTime { cols = append(cols, []string{"updated_unix"}...) sess.NoAutoTime() } return sess.Cols(cols...).Where(cond).Update(issue)} func changeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) { // Reload the issue currentIssue, err := GetIssueByID(ctx, issue.ID) if err != nil { return nil, err } // Nothing should be performed if current status is same as target status if currentIssue.IsClosed == isClosed { if !issue.IsPull { return nil, ErrIssueWasClosed{ ID: issue.ID, } } return nil, ErrPullWasClosed{ ID: issue.ID, } } issue.IsClosed = isClosed return doChangeIssueStatus(ctx, issue, doer, isMergePull)} func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) { if user_model.IsBlockedMultiple(ctx, []int64{issue.Repo.OwnerID, issue.PosterID}, doer.ID) { return nil, user_model.ErrBlockedByUser } // Check for open dependencies if issue.IsClosed && issue.Repo.IsDependenciesEnabled(ctx) { // only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies noDeps, err := IssueNoDependenciesLeft(ctx, issue) if err != nil { return nil, err } if !noDeps { return nil, ErrDependenciesLeft{issue.ID} } } if issue.IsClosed { if issue.NoAutoTime { issue.ClosedUnix = issue.UpdatedUnix } else { issue.ClosedUnix = timeutil.TimeStampNow() } } else { issue.ClosedUnix = 0 } if err := UpdateIssueCols(ctx, issue, "is_closed", "closed_unix"); err != nil { return nil, err } // Update issue count of labels if err := issue.LoadLabels(ctx); err != nil { return nil, err } for idx := range issue.Labels { if err := updateLabelCols(ctx, issue.Labels[idx], "num_issues", "num_closed_issue"); err != nil { return nil, err } } // Update issue count of milestone if issue.MilestoneID > 0 { if issue.NoAutoTime { if err := UpdateMilestoneCountersWithDate(ctx, issue.MilestoneID, issue.UpdatedUnix); err != nil { return nil, err } } else { if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { return nil, err } } } // update repository's issue closed number if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil { return nil, err } // New action comment cmtType := CommentTypeClose if !issue.IsClosed { cmtType = CommentTypeReopen } else if isMergePull { cmtType = CommentTypeMergePull } return CreateComment(ctx, &CreateCommentOptions{ Type: cmtType, Doer: doer, Repo: issue.Repo, Issue: issue, })} // ChangeIssueStatus changes issue status to open or closed.func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed bool) (*Comment, error) { if err := issue.LoadRepo(ctx); err != nil { return nil, err } if err := issue.LoadPoster(ctx); err != nil { return nil, err } return changeIssueStatus(ctx, issue, doer, isClosed, false)} // ChangeIssueTitle changes the title of this issue, as the given user.func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User, oldTitle string) (err error) { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) if err = UpdateIssueCols(ctx, issue, "name"); err != nil { return fmt.Errorf("updateIssueCols: %w", err) } if err = issue.LoadRepo(ctx); err != nil { return fmt.Errorf("loadRepo: %w", err) } opts := &CreateCommentOptions{ Type: CommentTypeChangeTitle, Doer: doer, Repo: issue.Repo, Issue: issue, OldTitle: oldTitle, NewTitle: issue.Title, } if _, err = CreateComment(ctx, opts); err != nil { return fmt.Errorf("createComment: %w", err) } if err = issue.AddCrossReferences(ctx, doer, true); err != nil { return fmt.Errorf("addCrossReferences: %w", err) } return committer.Commit()} // ChangeIssueRef changes the branch of this issue, as the given user.func ChangeIssueRef(ctx context.Context, issue *Issue, doer *user_model.User, oldRef string) (err error) { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() if err = UpdateIssueCols(ctx, issue, "ref"); err != nil { return fmt.Errorf("updateIssueCols: %w", err) } if err = issue.LoadRepo(ctx); err != nil { return fmt.Errorf("loadRepo: %w", err) } oldRefFriendly := strings.TrimPrefix(oldRef, git.BranchPrefix) newRefFriendly := strings.TrimPrefix(issue.Ref, git.BranchPrefix) opts := &CreateCommentOptions{ Type: CommentTypeChangeIssueRef, Doer: doer, Repo: issue.Repo, Issue: issue, OldRef: oldRefFriendly, NewRef: newRefFriendly, } if _, err = CreateComment(ctx, opts); err != nil { return fmt.Errorf("createComment: %w", err) } return committer.Commit()} // AddDeletePRBranchComment adds delete branch comment for pull request issuefunc AddDeletePRBranchComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issueID int64, branchName string) error { issue, err := GetIssueByID(ctx, issueID) if err != nil { return err } opts := &CreateCommentOptions{ Type: CommentTypeDeleteBranch, Doer: doer, Repo: repo, Issue: issue, OldRef: branchName, } _, err = CreateComment(ctx, opts) return err} // UpdateIssueAttachments update attachments by UUIDs for the issuefunc UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string) (err error) { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) if err != nil { return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) } for i := 0; i < len(attachments); i++ { attachments[i].IssueID = issueID if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) } } return committer.Commit()} // ChangeIssueContent changes issue content, as the given user.func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User, content string, contentVersion int) (err error) { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() hasContentHistory, err := HasIssueContentHistory(ctx, issue.ID, 0) if err != nil { return fmt.Errorf("HasIssueContentHistory: %w", err) } if !hasContentHistory { if err = SaveIssueContentHistory(ctx, issue.PosterID, issue.ID, 0, issue.CreatedUnix, issue.Content, true); err != nil { return fmt.Errorf("SaveIssueContentHistory: %w", err) } } issue.Content = content issue.ContentVersion = contentVersion + 1 expectedContentVersion := builder.NewCond().And(builder.Eq{"content_version": contentVersion}) affected, err := UpdateIssueColsWithCond(ctx, issue, expectedContentVersion, "content", "content_version") if err != nil { return fmt.Errorf("UpdateIssueCols: %w", err) } if affected == 0 { return ErrIssueAlreadyChanged } historyDate := timeutil.TimeStampNow() if issue.NoAutoTime { historyDate = issue.UpdatedUnix } if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0, historyDate, issue.Content, false); err != nil { return fmt.Errorf("SaveIssueContentHistory: %w", err) } if err = issue.AddCrossReferences(ctx, doer, true); err != nil { return fmt.Errorf("addCrossReferences: %w", err) } return committer.Commit()} // NewIssueOptions represents the options of a new issue.type NewIssueOptions struct { Repo *repo_model.Repository Issue *Issue LabelIDs []int64 Attachments []string // In UUID format. IsPull bool} // NewIssueWithIndex creates issue with given indexfunc NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssueOptions) (err error) { e := db.GetEngine(ctx) opts.Issue.Title = strings.TrimSpace(opts.Issue.Title) if opts.Issue.MilestoneID > 0 { milestone, err := GetMilestoneByRepoID(ctx, opts.Issue.RepoID, opts.Issue.MilestoneID) if err != nil && !IsErrMilestoneNotExist(err) { return fmt.Errorf("getMilestoneByID: %w", err) } // Assume milestone is invalid and drop silently. opts.Issue.MilestoneID = 0 if milestone != nil { opts.Issue.MilestoneID = milestone.ID opts.Issue.Milestone = milestone } } if opts.Issue.Index <= 0 { return fmt.Errorf("no issue index provided") } if opts.Issue.ID > 0 { return fmt.Errorf("issue exist") } opts.Issue.Created = timeutil.TimeStampNanoNow() if _, err := e.Insert(opts.Issue); err != nil { return err } if opts.Issue.MilestoneID > 0 { if err := UpdateMilestoneCounters(ctx, opts.Issue.MilestoneID); err != nil { return err } opts := &CreateCommentOptions{ Type: CommentTypeMilestone, Doer: doer, Repo: opts.Repo, Issue: opts.Issue, OldMilestoneID: 0, MilestoneID: opts.Issue.MilestoneID, } if _, err = CreateComment(ctx, opts); err != nil { return err } } if err := repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.IsPull, false); err != nil { return err } if len(opts.LabelIDs) > 0 { // During the session, SQLite3 driver cannot handle retrieve objects after update something. // So we have to get all needed labels first. labels := make([]*Label, 0, len(opts.LabelIDs)) if err = e.In("id", opts.LabelIDs).Find(&labels); err != nil { return fmt.Errorf("find all labels [label_ids: %v]: %w", opts.LabelIDs, err) } if err = opts.Issue.LoadPoster(ctx); err != nil { return err } for _, label := range labels { // Silently drop invalid labels. if label.RepoID != opts.Repo.ID && label.OrgID != opts.Repo.OwnerID { continue } if err = newIssueLabel(ctx, opts.Issue, label, opts.Issue.Poster); err != nil { return fmt.Errorf("addLabel [id: %d]: %w", label.ID, err) } } } if err = NewIssueUsers(ctx, opts.Repo, opts.Issue); err != nil { return err } if len(opts.Attachments) > 0 { attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) if err != nil { return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err) } for i := 0; i < len(attachments); i++ { attachments[i].IssueID = opts.Issue.ID if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil { return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) } } } if err = opts.Issue.LoadAttributes(ctx); err != nil { return err } return opts.Issue.AddCrossReferences(ctx, doer, false)} // NewIssue creates new issue with labels for repository.// The title will be cut off at 255 characters if it's longer than 255 characters.func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() idx, err := db.GetNextResourceIndex(ctx, "issue_index", repo.ID) if err != nil { return fmt.Errorf("generate issue index failed: %w", err) } issue.Index = idx issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ Repo: repo, Issue: issue, LabelIDs: labelIDs, Attachments: uuids, }); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { return err } return fmt.Errorf("newIssue: %w", err) } if err = committer.Commit(); err != nil { return fmt.Errorf("Commit: %w", err) } return nil} // UpdateIssueMentions updates issue-user relations for mentioned users.func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_model.User) error { if len(mentions) == 0 { return nil } ids := make([]int64, len(mentions)) for i, u := range mentions { ids[i] = u.ID } if err := UpdateIssueUsersByMentions(ctx, issueID, ids); err != nil { return fmt.Errorf("UpdateIssueUsersByMentions: %w", err) } return nil} // UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it.func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) { // if the deadline hasn't changed do nothing if issue.DeadlineUnix == deadlineUnix { return nil } ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() // Update the deadline if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix, NoAutoTime: issue.NoAutoTime, UpdatedUnix: issue.UpdatedUnix}, "deadline_unix"); err != nil { return err } // Make the comment if _, err = createDeadlineComment(ctx, doer, issue, deadlineUnix); err != nil { return fmt.Errorf("createRemovedDueDateComment: %w", err) } return committer.Commit()} // FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database.func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) { rawMentions := references.FindAllMentionsMarkdown(content) mentions, err = ResolveIssueMentionsByVisibility(ctx, issue, doer, rawMentions) if err != nil { return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err) } if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {Lines 1–500 of 803. Load more
// Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package issues import ( "context" "fmt" "strings" "forgejo.org/models/db" "forgejo.org/models/organization" "forgejo.org/models/perm" access_model "forgejo.org/models/perm/access" project_model "forgejo.org/models/project" repo_model "forgejo.org/models/repo" system_model "forgejo.org/models/system" "forgejo.org/models/unit" user_model "forgejo.org/models/user" "forgejo.org/modules/git" "forgejo.org/modules/references" api "forgejo.org/modules/structs" "forgejo.org/modules/timeutil" "forgejo.org/modules/util" "xorm.io/builder" ) func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error { _, err := UpdateIssueColsWithCond(ctx, issue, builder.NewCond(), cols...) return err } func UpdateIssueColsWithCond(ctx context.Context, issue *Issue, cond builder.Cond, cols ...string) (int64, error) { sess := db.GetEngine(ctx).ID(issue.ID) if issue.NoAutoTime { cols = append(cols, []string{"updated_unix"}...) sess.NoAutoTime() } return sess.Cols(cols...).Where(cond).Update(issue) } func changeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) { // Reload the issue currentIssue, err := GetIssueByID(ctx, issue.ID) if err != nil { return nil, err } // Nothing should be performed if current status is same as target status if currentIssue.IsClosed == isClosed { if !issue.IsPull { return nil, ErrIssueWasClosed{ ID: issue.ID, } } return nil, ErrPullWasClosed{ ID: issue.ID, } } issue.IsClosed = isClosed return doChangeIssueStatus(ctx, issue, doer, isMergePull) } func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) { if user_model.IsBlockedMultiple(ctx, []int64{issue.Repo.OwnerID, issue.PosterID}, doer.ID) { return nil, user_model.ErrBlockedByUser } // Check for open dependencies if issue.IsClosed && issue.Repo.IsDependenciesEnabled(ctx) { // only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies noDeps, err := IssueNoDependenciesLeft(ctx, issue) if err != nil { return nil, err } if !noDeps { return nil, ErrDependenciesLeft{issue.ID} } } if issue.IsClosed { if issue.NoAutoTime { issue.ClosedUnix = issue.UpdatedUnix } else { issue.ClosedUnix = timeutil.TimeStampNow() } } else { issue.ClosedUnix = 0 } if err := UpdateIssueCols(ctx, issue, "is_closed", "closed_unix"); err != nil { return nil, err } // Update issue count of labels if err := issue.LoadLabels(ctx); err != nil { return nil, err } for idx := range issue.Labels { if err := updateLabelCols(ctx, issue.Labels[idx], "num_issues", "num_closed_issue"); err != nil { return nil, err } } // Update issue count of milestone if issue.MilestoneID > 0 { if issue.NoAutoTime { if err := UpdateMilestoneCountersWithDate(ctx, issue.MilestoneID, issue.UpdatedUnix); err != nil { return nil, err } } else { if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { return nil, err } } } // update repository's issue closed number if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil { return nil, err } // New action comment cmtType := CommentTypeClose if !issue.IsClosed { cmtType = CommentTypeReopen } else if isMergePull { cmtType = CommentTypeMergePull } return CreateComment(ctx, &CreateCommentOptions{ Type: cmtType, Doer: doer, Repo: issue.Repo, Issue: issue, }) } // ChangeIssueStatus changes issue status to open or closed. func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed bool) (*Comment, error) { if err := issue.LoadRepo(ctx); err != nil { return nil, err } if err := issue.LoadPoster(ctx); err != nil { return nil, err } return changeIssueStatus(ctx, issue, doer, isClosed, false) } // ChangeIssueTitle changes the title of this issue, as the given user. func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User, oldTitle string) (err error) { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) if err = UpdateIssueCols(ctx, issue, "name"); err != nil { return fmt.Errorf("updateIssueCols: %w", err) } if err = issue.LoadRepo(ctx); err != nil { return fmt.Errorf("loadRepo: %w", err) } opts := &CreateCommentOptions{ Type: CommentTypeChangeTitle, Doer: doer, Repo: issue.Repo, Issue: issue, OldTitle: oldTitle, NewTitle: issue.Title, } if _, err = CreateComment(ctx, opts); err != nil { return fmt.Errorf("createComment: %w", err) } if err = issue.AddCrossReferences(ctx, doer, true); err != nil { return fmt.Errorf("addCrossReferences: %w", err) } return committer.Commit() } // ChangeIssueRef changes the branch of this issue, as the given user. func ChangeIssueRef(ctx context.Context, issue *Issue, doer *user_model.User, oldRef string) (err error) { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() if err = UpdateIssueCols(ctx, issue, "ref"); err != nil { return fmt.Errorf("updateIssueCols: %w", err) } if err = issue.LoadRepo(ctx); err != nil { return fmt.Errorf("loadRepo: %w", err) } oldRefFriendly := strings.TrimPrefix(oldRef, git.BranchPrefix) newRefFriendly := strings.TrimPrefix(issue.Ref, git.BranchPrefix) opts := &CreateCommentOptions{ Type: CommentTypeChangeIssueRef, Doer: doer, Repo: issue.Repo, Issue: issue, OldRef: oldRefFriendly, NewRef: newRefFriendly, } if _, err = CreateComment(ctx, opts); err != nil { return fmt.Errorf("createComment: %w", err) } return committer.Commit() } // AddDeletePRBranchComment adds delete branch comment for pull request issue func AddDeletePRBranchComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issueID int64, branchName string) error { issue, err := GetIssueByID(ctx, issueID) if err != nil { return err } opts := &CreateCommentOptions{ Type: CommentTypeDeleteBranch, Doer: doer, Repo: repo, Issue: issue, OldRef: branchName, } _, err = CreateComment(ctx, opts) return err } // UpdateIssueAttachments update attachments by UUIDs for the issue func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string) (err error) { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids) if err != nil { return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) } for i := 0; i < len(attachments); i++ { attachments[i].IssueID = issueID if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) } } return committer.Commit() } // ChangeIssueContent changes issue content, as the given user. func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User, content string, contentVersion int) (err error) { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() hasContentHistory, err := HasIssueContentHistory(ctx, issue.ID, 0) if err != nil { return fmt.Errorf("HasIssueContentHistory: %w", err) } if !hasContentHistory { if err = SaveIssueContentHistory(ctx, issue.PosterID, issue.ID, 0, issue.CreatedUnix, issue.Content, true); err != nil { return fmt.Errorf("SaveIssueContentHistory: %w", err) } } issue.Content = content issue.ContentVersion = contentVersion + 1 expectedContentVersion := builder.NewCond().And(builder.Eq{"content_version": contentVersion}) affected, err := UpdateIssueColsWithCond(ctx, issue, expectedContentVersion, "content", "content_version") if err != nil { return fmt.Errorf("UpdateIssueCols: %w", err) } if affected == 0 { return ErrIssueAlreadyChanged } historyDate := timeutil.TimeStampNow() if issue.NoAutoTime { historyDate = issue.UpdatedUnix } if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0, historyDate, issue.Content, false); err != nil { return fmt.Errorf("SaveIssueContentHistory: %w", err) } if err = issue.AddCrossReferences(ctx, doer, true); err != nil { return fmt.Errorf("addCrossReferences: %w", err) } return committer.Commit() } // NewIssueOptions represents the options of a new issue. type NewIssueOptions struct { Repo *repo_model.Repository Issue *Issue LabelIDs []int64 Attachments []string // In UUID format. IsPull bool } // NewIssueWithIndex creates issue with given index func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssueOptions) (err error) { e := db.GetEngine(ctx) opts.Issue.Title = strings.TrimSpace(opts.Issue.Title) if opts.Issue.MilestoneID > 0 { milestone, err := GetMilestoneByRepoID(ctx, opts.Issue.RepoID, opts.Issue.MilestoneID) if err != nil && !IsErrMilestoneNotExist(err) { return fmt.Errorf("getMilestoneByID: %w", err) } // Assume milestone is invalid and drop silently. opts.Issue.MilestoneID = 0 if milestone != nil { opts.Issue.MilestoneID = milestone.ID opts.Issue.Milestone = milestone } } if opts.Issue.Index <= 0 { return fmt.Errorf("no issue index provided") } if opts.Issue.ID > 0 { return fmt.Errorf("issue exist") } opts.Issue.Created = timeutil.TimeStampNanoNow() if _, err := e.Insert(opts.Issue); err != nil { return err } if opts.Issue.MilestoneID > 0 { if err := UpdateMilestoneCounters(ctx, opts.Issue.MilestoneID); err != nil { return err } opts := &CreateCommentOptions{ Type: CommentTypeMilestone, Doer: doer, Repo: opts.Repo, Issue: opts.Issue, OldMilestoneID: 0, MilestoneID: opts.Issue.MilestoneID, } if _, err = CreateComment(ctx, opts); err != nil { return err } } if err := repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.IsPull, false); err != nil { return err } if len(opts.LabelIDs) > 0 { // During the session, SQLite3 driver cannot handle retrieve objects after update something. // So we have to get all needed labels first. labels := make([]*Label, 0, len(opts.LabelIDs)) if err = e.In("id", opts.LabelIDs).Find(&labels); err != nil { return fmt.Errorf("find all labels [label_ids: %v]: %w", opts.LabelIDs, err) } if err = opts.Issue.LoadPoster(ctx); err != nil { return err } for _, label := range labels { // Silently drop invalid labels. if label.RepoID != opts.Repo.ID && label.OrgID != opts.Repo.OwnerID { continue } if err = newIssueLabel(ctx, opts.Issue, label, opts.Issue.Poster); err != nil { return fmt.Errorf("addLabel [id: %d]: %w", label.ID, err) } } } if err = NewIssueUsers(ctx, opts.Repo, opts.Issue); err != nil { return err } if len(opts.Attachments) > 0 { attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) if err != nil { return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err) } for i := 0; i < len(attachments); i++ { attachments[i].IssueID = opts.Issue.ID if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil { return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) } } } if err = opts.Issue.LoadAttributes(ctx); err != nil { return err } return opts.Issue.AddCrossReferences(ctx, doer, false) } // NewIssue creates new issue with labels for repository. // The title will be cut off at 255 characters if it's longer than 255 characters. func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() idx, err := db.GetNextResourceIndex(ctx, "issue_index", repo.ID) if err != nil { return fmt.Errorf("generate issue index failed: %w", err) } issue.Index = idx issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255) if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ Repo: repo, Issue: issue, LabelIDs: labelIDs, Attachments: uuids, }); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { return err } return fmt.Errorf("newIssue: %w", err) } if err = committer.Commit(); err != nil { return fmt.Errorf("Commit: %w", err) } return nil } // UpdateIssueMentions updates issue-user relations for mentioned users. func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_model.User) error { if len(mentions) == 0 { return nil } ids := make([]int64, len(mentions)) for i, u := range mentions { ids[i] = u.ID } if err := UpdateIssueUsersByMentions(ctx, issueID, ids); err != nil { return fmt.Errorf("UpdateIssueUsersByMentions: %w", err) } return nil } // UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it. func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) { // if the deadline hasn't changed do nothing if issue.DeadlineUnix == deadlineUnix { return nil } ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() // Update the deadline if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix, NoAutoTime: issue.NoAutoTime, UpdatedUnix: issue.UpdatedUnix}, "deadline_unix"); err != nil { return err } // Make the comment if _, err = createDeadlineComment(ctx, doer, issue, deadlineUnix); err != nil { return fmt.Errorf("createRemovedDueDateComment: %w", err) } return committer.Commit() } // FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database. func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) { rawMentions := references.FindAllMentionsMarkdown(content) mentions, err = ResolveIssueMentionsByVisibility(ctx, issue, doer, rawMentions) if err != nil { return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err) } if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {