From d37f246d09ca0ea94cceed25befd2c366b5eb2b5 Mon Sep 17 00:00:00 2001 From: splitt3r Date: Sat, 19 Aug 2023 10:25:06 +0000 Subject: [PATCH 01/26] Add reviewers selection to new pull request form --- routers/api/v1/repo/pull.go | 2 +- routers/web/repo/compare.go | 43 ++ routers/web/repo/issue.go | 53 +- routers/web/repo/pull.go | 4 +- services/agit/agit.go | 2 +- services/forms/repo_form.go | 1 + services/pull/pull.go | 25 +- templates/repo/issue/new_form.tmpl | 52 ++ tests/integration/actions_trigger_test.go | 4 +- tests/integration/pull_merge_test.go | 2 +- tests/integration/pull_update_test.go | 2 +- web_src/js/features/repo-legacy.js | 600 ++++++++++++++++++++++ 12 files changed, 768 insertions(+), 22 deletions(-) create mode 100644 web_src/js/features/repo-legacy.js diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 34ebcb42d5aef..0f98507a7eb53 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -554,7 +554,7 @@ func CreatePullRequest(ctx *context.APIContext) { } } - if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil { + if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs, []int64{}); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err) } else if errors.Is(err, user_model.ErrBlockedUser) { diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index e6a04782e5bbf..70d7d37d6599f 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" @@ -39,6 +40,7 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/gitdiff" + repo_service "code.gitea.io/gitea/services/repository" ) const ( @@ -789,6 +791,47 @@ func CompareDiff(ctx *context.Context) { if ctx.Written() { return } + + // Get reviewer info for pr + var ( + reviewers []*user_model.User + teamReviewers []*organization.Team + reviewersResult []*repoReviewerSelection + ) + reviewers, err = repo_model.GetReviewers(ctx, ctx.Repo.Repository, ctx.Doer.ID, ctx.Doer.ID) + if err != nil { + ctx.ServerError("GetReviewers", err) + return + } + + teamReviewers, err = repo_service.GetReviewerTeams(ctx, ctx.Repo.Repository) + if err != nil { + ctx.ServerError("GetReviewerTeams", err) + return + } + + for _, user := range reviewers { + reviewersResult = append(reviewersResult, &repoReviewerSelection{ + IsTeam: false, + CanChange: true, + User: user, + ItemID: user.ID, + }) + } + + // negative reviewIDs represent team requests + for _, team := range teamReviewers { + reviewersResult = append(reviewersResult, &repoReviewerSelection{ + IsTeam: true, + CanChange: true, + Team: team, + ItemID: -team.ID, + }) + } + ctx.Data["Reviewers"] = reviewersResult + if ctx.Written() { + return + } } } beforeCommitID := ctx.Data["BeforeCommitID"].(string) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 507b5af9d904a..136dd66e5ad4f 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1118,7 +1118,7 @@ func DeleteIssue(ctx *context.Context) { } // ValidateRepoMetas check and returns repository's meta information -func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) { +func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, []int64, int64, int64) { var ( repo = ctx.Repo.Repository err error @@ -1126,7 +1126,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull) if ctx.Written() { - return nil, nil, 0, 0 + return nil, nil, nil, 0, 0 } var labelIDs []int64 @@ -1135,7 +1135,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull if len(form.LabelIDs) > 0 { labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ",")) if err != nil { - return nil, nil, 0, 0 + return nil, nil, nil, 0, 0 } labelIDMark := make(container.Set[int64]) labelIDMark.AddMultiple(labelIDs...) @@ -1158,11 +1158,11 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID) if err != nil { ctx.ServerError("GetMilestoneByID", err) - return nil, nil, 0, 0 + return nil, nil, nil, 0, 0 } if milestone.RepoID != repo.ID { ctx.ServerError("GetMilestoneByID", err) - return nil, nil, 0, 0 + return nil, nil, nil, 0, 0 } ctx.Data["Milestone"] = milestone ctx.Data["milestone_id"] = milestoneID @@ -1172,11 +1172,11 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull p, err := project_model.GetProjectByID(ctx, form.ProjectID) if err != nil { ctx.ServerError("GetProjectByID", err) - return nil, nil, 0, 0 + return nil, nil, nil, 0, 0 } if p.RepoID != ctx.Repo.Repository.ID && p.OwnerID != ctx.Repo.Repository.OwnerID { ctx.NotFound("", nil) - return nil, nil, 0, 0 + return nil, nil, nil, 0, 0 } ctx.Data["Project"] = p @@ -1188,7 +1188,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull if len(form.AssigneeIDs) > 0 { assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ",")) if err != nil { - return nil, nil, 0, 0 + return nil, nil, nil, 0, 0 } // Check if the passed assignees actually exists and is assignable @@ -1196,18 +1196,18 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull assignee, err := user_model.GetUserByID(ctx, aID) if err != nil { ctx.ServerError("GetUserByID", err) - return nil, nil, 0, 0 + return nil, nil, nil, 0, 0 } valid, err := access_model.CanBeAssigned(ctx, assignee, repo, isPull) if err != nil { ctx.ServerError("CanBeAssigned", err) - return nil, nil, 0, 0 + return nil, nil, nil, 0, 0 } if !valid { ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}) - return nil, nil, 0, 0 + return nil, nil, nil, 0, 0 } } } @@ -1217,7 +1217,34 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull assigneeIDs = append(assigneeIDs, form.AssigneeID) } - return labelIDs, assigneeIDs, milestoneID, form.ProjectID + // Check reviewers + var reviewerIDs []int64 + if len(form.ReviewerIDs) > 0 { + reviewerIDs, err = base.StringsToInt64s(strings.Split(form.ReviewerIDs, ",")) + if err != nil { + return nil, nil, nil, 0, 0 + } + + // Check if the passed reviewers actually exist + for _, rID := range reviewerIDs { + if rID < 0 { + _, err := organization.GetTeamByID(ctx, -rID) + if err != nil { + ctx.ServerError("GetTeamByID", err) + return nil, nil, nil, 0, 0 + } + continue + } + + _, err := user_model.GetUserByID(ctx, rID) + if err != nil { + ctx.ServerError("GetUserByID", err) + return nil, nil, nil, 0, 0 + } + } + } + + return labelIDs, assigneeIDs, reviewerIDs, milestoneID, form.ProjectID } // NewIssuePost response for creating new issue @@ -1235,7 +1262,7 @@ func NewIssuePost(ctx *context.Context) { attachments []string ) - labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false) + labelIDs, assigneeIDs, _, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false) if ctx.Written() { return } diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 0efe1be76a310..05b47faeff130 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1268,7 +1268,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { return } - labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, true) + labelIDs, assigneeIDs, reviewerIDs, milestoneID, _ := ValidateRepoMetas(ctx, *form, true) if ctx.Written() { return } @@ -1318,7 +1318,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { // FIXME: check error in the case two people send pull request at almost same time, give nice error prompt // instead of 500. - if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil { + if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs, reviewerIDs); err != nil { switch { case repo_model.IsErrUserDoesNotHaveAccessToRepo(err): ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) diff --git a/services/agit/agit.go b/services/agit/agit.go index 82aa2791aa76d..b4ebb2eb18296 100644 --- a/services/agit/agit.go +++ b/services/agit/agit.go @@ -138,7 +138,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. Flow: issues_model.PullRequestFlowAGit, } - if err := pull_service.NewPullRequest(ctx, repo, prIssue, []int64{}, []string{}, pr, []int64{}); err != nil { + if err := pull_service.NewPullRequest(ctx, repo, prIssue, []int64{}, []string{}, pr, []int64{}, []int64{}); err != nil { return nil, err } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index ddd07a64cbf75..83f2dd6caacfc 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -447,6 +447,7 @@ type CreateIssueForm struct { Title string `binding:"Required;MaxSize(255)"` LabelIDs string `form:"label_ids"` AssigneeIDs string `form:"assignee_ids"` + ReviewerIDs string `form:"reviewer_ids"` Ref string `form:"ref"` MilestoneID int64 ProjectID int64 diff --git a/services/pull/pull.go b/services/pull/pull.go index bab4e49998e15..68e81814deb56 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" @@ -42,7 +43,7 @@ func getPullWorkingLockKey(prID int64) string { } // NewPullRequest creates new pull request with labels for repository. -func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error { +func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64, reviewerIDs []int64) error { if err := issue.LoadPoster(ctx); err != nil { return err } @@ -116,6 +117,28 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss assigneeCommentMap[assigneeID] = comment } + for _, reviewerID := range reviewerIDs { + if reviewerID < 0 { + team, err := organization.GetTeamByID(ctx, -reviewerID) + if err != nil { + return err + } + _, err = issue_service.TeamReviewRequest(ctx, issue, issue.Poster, team, true) + if err != nil { + return err + } + } + + reviewer, err := user_model.GetUserByID(ctx, reviewerID) + if err != nil { + return err + } + _, err = issue_service.ReviewRequest(ctx, issue, issue.Poster, reviewer, true) + if err != nil { + return err + } + } + pr.Issue = issue issue.PullRequest = pr diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index e56d1b9ecc7bd..db7a44fec3c09 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -48,6 +48,58 @@
{{template "repo/issue/branch_selector_field" .}} + {{if .PageIsComparePull}} + + +
+ + {{.locale.Tr "repo.issues.new.no_reviewers"}} + + +
+
+ {{end}} {{template "repo/issue/labels/labels_selector_field" .}} diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index c254c90958314..4a542884fca2b 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -145,7 +145,7 @@ func TestPullRequestTargetEvent(t *testing.T) { BaseRepo: baseRepo, Type: issues_model.PullRequestGitea, } - err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil) + err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil, nil) assert.NoError(t, err) // load and compare ActionRun @@ -199,7 +199,7 @@ func TestPullRequestTargetEvent(t *testing.T) { BaseRepo: baseRepo, Type: issues_model.PullRequestGitea, } - err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil) + err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil, nil) assert.NoError(t, err) // the new pull request cannot trigger actions, so there is still only 1 record diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 43210e852e53b..729d22facfb33 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -520,7 +520,7 @@ func TestConflictChecking(t *testing.T) { BaseRepo: baseRepo, Type: issues_model.PullRequestGitea, } - err = pull.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil) + err = pull.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil, nil) assert.NoError(t, err) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"}) diff --git a/tests/integration/pull_update_test.go b/tests/integration/pull_update_test.go index 5ae241f3af720..c283488563a8a 100644 --- a/tests/integration/pull_update_test.go +++ b/tests/integration/pull_update_test.go @@ -173,7 +173,7 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod BaseRepo: baseRepo, Type: issues_model.PullRequestGitea, } - err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil) + err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil, nil) assert.NoError(t, err) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "Test Pull -to-update-"}) diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js new file mode 100644 index 0000000000000..9b1307d7bcd63 --- /dev/null +++ b/web_src/js/features/repo-legacy.js @@ -0,0 +1,600 @@ +import $ from 'jquery'; +import { + initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete, + initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue, + initRepoIssueTitleEdit, initRepoIssueWipToggle, + initRepoPullRequestUpdate, updateIssuesMeta, handleReply, initIssueTemplateCommentEditors, initSingleCommentEditor, +} from './repo-issue.js'; +import {initUnicodeEscapeButton} from './repo-unicode-escape.js'; +import {svg} from '../svg.js'; +import {htmlEscape} from 'escape-goat'; +import {initRepoBranchTagSelector} from '../components/RepoBranchTagSelector.vue'; +import { + initRepoCloneLink, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown, + initRepoCommonLanguageStats, +} from './repo-common.js'; +import {initCitationFileCopyContent} from './citation.js'; +import {initCompLabelEdit} from './comp/LabelEdit.js'; +import {initRepoDiffConversationNav} from './repo-diff.js'; +import {createDropzone} from './dropzone.js'; +import {initCommentContent, initMarkupContent} from '../markup/content.js'; +import {initCompReactionSelector} from './comp/ReactionSelector.js'; +import {initRepoSettingBranches} from './repo-settings.js'; +import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.js'; +import {hideElem, showElem} from '../utils/dom.js'; +import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; +import {attachRefIssueContextPopup} from './contextpopup.js'; + +const {csrfToken} = window.config; + +// if there are draft comments, confirm before reloading, to avoid losing comments +function reloadConfirmDraftComment() { + const commentTextareas = [ + document.querySelector('.edit-content-zone:not(.gt-hidden) textarea'), + document.querySelector('#comment-form textarea'), + ]; + for (const textarea of commentTextareas) { + // Most users won't feel too sad if they lose a comment with 10 chars, they can re-type these in seconds. + // But if they have typed more (like 50) chars and the comment is lost, they will be very unhappy. + if (textarea && textarea.value.trim().length > 10) { + textarea.parentElement.scrollIntoView(); + if (!window.confirm('Page will be reloaded, but there are draft comments. Continuing to reload will discard the comments. Continue?')) { + return; + } + break; + } + } + window.location.reload(); +} + +export function initRepoCommentForm() { + const $commentForm = $('.comment.form'); + if ($commentForm.length === 0) { + return; + } + + if ($commentForm.find('.field.combo-editor-dropzone').length) { + // at the moment, if a form has multiple combo-markdown-editors, it must be a issue template form + initIssueTemplateCommentEditors($commentForm); + } else { + initSingleCommentEditor($commentForm); + } + + function initBranchSelector() { + const $selectBranch = $('.ui.select-branch'); + const $branchMenu = $selectBranch.find('.reference-list-menu'); + const $isNewIssue = $branchMenu.hasClass('new-issue'); + $branchMenu.find('.item:not(.no-select)').on('click', function () { + const selectedValue = $(this).data('id'); + const editMode = $('#editing_mode').val(); + $($(this).data('id-selector')).val(selectedValue); + if ($isNewIssue) { + $selectBranch.find('.ui .branch-name').text($(this).data('name')); + return; + } + + if (editMode === 'true') { + const form = $('#update_issueref_form'); + $.post(form.attr('action'), {_csrf: csrfToken, ref: selectedValue}, () => window.location.reload()); + } else if (editMode === '') { + $selectBranch.find('.ui .branch-name').text(selectedValue); + } + }); + $selectBranch.find('.reference.column').on('click', function () { + hideElem($selectBranch.find('.scrolling.reference-list-menu')); + $selectBranch.find('.reference .text').removeClass('black'); + showElem($($(this).data('target'))); + $(this).find('.text').addClass('black'); + return false; + }); + } + + initBranchSelector(); + + // List submits + function initListSubmits(selector, outerSelector) { + const $list = $(`.ui.${outerSelector}.list`); + const $noSelect = $list.find('.no-select'); + const $listMenu = $(`.${selector} .menu`); + let hasUpdateAction = $listMenu.data('action') === 'update'; + const items = {}; + + $(`.${selector}`).dropdown({ + 'action': 'nothing', // do not hide the menu if user presses Enter + fullTextSearch: 'exact', + async onHide() { + hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var + if (hasUpdateAction) { + // TODO: Add batch functionality and make this 1 network request. + const itemEntries = Object.entries(items); + for (const [elementId, item] of itemEntries) { + await updateIssuesMeta( + item['update-url'], + item.action, + item['issue-id'], + elementId, + ); + } + if (itemEntries.length) { + reloadConfirmDraftComment(); + } + } + }, + }); + + $listMenu.find('.item:not(.no-select)').on('click', function (e) { + e.preventDefault(); + if ($(this).hasClass('ban-change')) { + return false; + } + + hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var + + const clickedItem = $(this); + const scope = $(this).attr('data-scope'); + + $(this).parent().find('.item').each(function () { + if (scope) { + // Enable only clicked item for scoped labels + if ($(this).attr('data-scope') !== scope) { + return true; + } + if (!$(this).is(clickedItem) && !$(this).hasClass('checked')) { + return true; + } + } else if (!$(this).is(clickedItem)) { + // Toggle for other labels + return true; + } + + if ($(this).hasClass('checked')) { + $(this).removeClass('checked'); + $(this).find('.octicon-check').addClass('invisible'); + if (hasUpdateAction) { + if (!($(this).data('id') in items)) { + items[$(this).data('id')] = { + 'update-url': $listMenu.data('update-url'), + action: 'detach', + 'issue-id': $listMenu.data('issue-id'), + }; + } else { + delete items[$(this).data('id')]; + } + } + } else { + $(this).addClass('checked'); + $(this).find('.octicon-check').removeClass('invisible'); + if (hasUpdateAction) { + if (!($(this).data('id') in items)) { + items[$(this).data('id')] = { + 'update-url': $listMenu.data('update-url'), + action: 'attach', + 'issue-id': $listMenu.data('issue-id'), + }; + } else { + delete items[$(this).data('id')]; + } + } + } + }); + + // TODO: Which thing should be done for choosing review requests + // to make chosen items be shown on time here? + if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') { + return false; + } + + const listIds = []; + $(this).parent().find('.item').each(function () { + if ($(this).hasClass('checked')) { + listIds.push($(this).data('id')); + $($(this).data('id-selector')).removeClass('gt-hidden'); + } else { + $($(this).data('id-selector')).addClass('gt-hidden'); + } + }); + if (listIds.length === 0) { + $noSelect.removeClass('gt-hidden'); + } else { + $noSelect.addClass('gt-hidden'); + } + $($(this).parent().data('id')).val(listIds.join(',')); + return false; + }); + $listMenu.find('.no-select.item').on('click', function (e) { + e.preventDefault(); + if (hasUpdateAction) { + updateIssuesMeta( + $listMenu.data('update-url'), + 'clear', + $listMenu.data('issue-id'), + '', + ).then(reloadConfirmDraftComment); + } + + $(this).parent().find('.item').each(function () { + $(this).removeClass('checked'); + $(this).find('.octicon-check').addClass('invisible'); + }); + + if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') { + return false; + } + + $list.find('.item').each(function () { + $(this).addClass('gt-hidden'); + }); + $noSelect.removeClass('gt-hidden'); + $($(this).parent().data('id')).val(''); + }); + } + + // Init labels and assignees + initListSubmits('select-label', 'labels'); + initListSubmits('select-assignees', 'assignees'); + initListSubmits('select-assignees-modify', 'assignees'); + initListSubmits('select-reviewers', 'reviewers'); + initListSubmits('select-reviewers-modify', 'reviewers'); + + function selectItem(select_id, input_id) { + const $menu = $(`${select_id} .menu`); + const $list = $(`.ui${select_id}.list`); + const hasUpdateAction = $menu.data('action') === 'update'; + + $menu.find('.item:not(.no-select)').on('click', function () { + $(this).parent().find('.item').each(function () { + $(this).removeClass('selected active'); + }); + + $(this).addClass('selected active'); + if (hasUpdateAction) { + updateIssuesMeta( + $menu.data('update-url'), + '', + $menu.data('issue-id'), + $(this).data('id'), + ).then(reloadConfirmDraftComment); + } + + let icon = ''; + if (input_id === '#milestone_id') { + icon = svg('octicon-milestone', 18, 'gt-mr-3'); + } else if (input_id === '#project_id') { + icon = svg('octicon-project', 18, 'gt-mr-3'); + } else if (input_id === '#assignee_id') { + icon = `avatar`; + } else if (input_id === '#reviewer_id') { + icon = `avatar`; + } + + $list.find('.selected').html(` + + ${icon} + ${htmlEscape($(this).text())} + + `); + + $(`.ui${select_id}.list .no-select`).addClass('gt-hidden'); + $(input_id).val($(this).data('id')); + }); + $menu.find('.no-select.item').on('click', function () { + $(this).parent().find('.item:not(.no-select)').each(function () { + $(this).removeClass('selected active'); + }); + + if (hasUpdateAction) { + updateIssuesMeta( + $menu.data('update-url'), + '', + $menu.data('issue-id'), + $(this).data('id'), + ).then(reloadConfirmDraftComment); + } + + $list.find('.selected').html(''); + $list.find('.no-select').removeClass('gt-hidden'); + $(input_id).val(''); + }); + } + + // Milestone, Assignee, Project + selectItem('.select-project', '#project_id'); + selectItem('.select-milestone', '#milestone_id'); + selectItem('.select-assignee', '#assignee_id'); + selectItem('.select-reviewer', '#reviewer_id'); +} + + +async function onEditContent(event) { + event.preventDefault(); + + const $segment = $(this).closest('.header').next(); + const $editContentZone = $segment.find('.edit-content-zone'); + const $renderContent = $segment.find('.render-content'); + const $rawContent = $segment.find('.raw-content'); + + let comboMarkdownEditor; + + const setupDropzone = async ($dropzone) => { + if ($dropzone.length === 0) return null; + + let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event + let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone + const dz = await createDropzone($dropzone[0], { + url: $dropzone.attr('data-upload-url'), + headers: {'X-Csrf-Token': csrfToken}, + maxFiles: $dropzone.attr('data-max-file'), + maxFilesize: $dropzone.attr('data-max-size'), + acceptedFiles: (['*/*', ''].includes($dropzone.attr('data-accepts'))) ? null : $dropzone.attr('data-accepts'), + addRemoveLinks: true, + dictDefaultMessage: $dropzone.attr('data-default-message'), + dictInvalidFileType: $dropzone.attr('data-invalid-input-type'), + dictFileTooBig: $dropzone.attr('data-file-too-big'), + dictRemoveFile: $dropzone.attr('data-remove-file'), + timeout: 0, + thumbnailMethod: 'contain', + thumbnailWidth: 480, + thumbnailHeight: 480, + init() { + this.on('success', (file, data) => { + file.uuid = data.uuid; + fileUuidDict[file.uuid] = {submitted: false}; + const input = $(``).val(data.uuid); + $dropzone.find('.files').append(input); + }); + this.on('removedfile', (file) => { + if (disableRemovedfileEvent) return; + $(`#${file.uuid}`).remove(); + if ($dropzone.attr('data-remove-url') && !fileUuidDict[file.uuid].submitted) { + $.post($dropzone.attr('data-remove-url'), { + file: file.uuid, + _csrf: csrfToken, + }); + } + }); + this.on('submit', () => { + $.each(fileUuidDict, (fileUuid) => { + fileUuidDict[fileUuid].submitted = true; + }); + }); + this.on('reload', () => { + $.getJSON($editContentZone.attr('data-attachment-url'), (data) => { + // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server + disableRemovedfileEvent = true; + dz.removeAllFiles(true); + $dropzone.find('.files').empty(); + fileUuidDict = {}; + disableRemovedfileEvent = false; + + for (const attachment of data) { + const imgSrc = `${$dropzone.attr('data-link-url')}/${attachment.uuid}`; + dz.emit('addedfile', attachment); + dz.emit('thumbnail', attachment, imgSrc); + dz.emit('complete', attachment); + dz.files.push(attachment); + fileUuidDict[attachment.uuid] = {submitted: true}; + $dropzone.find(`img[src='${imgSrc}']`).css('max-width', '100%'); + const input = $(``).val(attachment.uuid); + $dropzone.find('.files').append(input); + } + }); + }); + }, + }); + dz.emit('reload'); + return dz; + }; + + const cancelAndReset = (dz) => { + showElem($renderContent); + hideElem($editContentZone); + if (dz) { + dz.emit('reload'); + } + }; + + const saveAndRefresh = (dz, $dropzone) => { + showElem($renderContent); + hideElem($editContentZone); + const $attachments = $dropzone.find('.files').find('[name=files]').map(function () { + return $(this).val(); + }).get(); + $.post($editContentZone.attr('data-update-url'), { + _csrf: csrfToken, + content: comboMarkdownEditor.value(), + context: $editContentZone.attr('data-context'), + files: $attachments, + }, (data) => { + if (!data.content) { + $renderContent.html($('#no-content').html()); + $rawContent.text(''); + } else { + $renderContent.html(data.content); + $rawContent.text(comboMarkdownEditor.value()); + + const refIssues = $renderContent.find('p .ref-issue'); + attachRefIssueContextPopup(refIssues); + } + const $content = $segment; + if (!$content.find('.dropzone-attachments').length) { + if (data.attachments !== '') { + $content.append(`
`); + $content.find('.dropzone-attachments').replaceWith(data.attachments); + } + } else if (data.attachments === '') { + $content.find('.dropzone-attachments').remove(); + } else { + $content.find('.dropzone-attachments').replaceWith(data.attachments); + } + if (dz) { + dz.emit('submit'); + dz.emit('reload'); + } + initMarkupContent(); + initCommentContent(); + }); + }; + + if (!$editContentZone.html()) { + $editContentZone.html($('#issue-comment-editor-template').html()); + comboMarkdownEditor = await initComboMarkdownEditor($editContentZone.find('.combo-markdown-editor')); + + const $dropzone = $editContentZone.find('.dropzone'); + const dz = await setupDropzone($dropzone); + $editContentZone.find('.cancel.button').on('click', (e) => { + e.preventDefault(); + cancelAndReset(dz); + }); + $editContentZone.find('.save.button').on('click', (e) => { + e.preventDefault(); + saveAndRefresh(dz, $dropzone); + }); + } else { + comboMarkdownEditor = getComboMarkdownEditor($editContentZone.find('.combo-markdown-editor')); + } + + // Show write/preview tab and copy raw content as needed + showElem($editContentZone); + hideElem($renderContent); + if (!comboMarkdownEditor.value()) { + comboMarkdownEditor.value($rawContent.text()); + } + comboMarkdownEditor.focus(); +} + +export function initRepository() { + if ($('.page-content.repository').length === 0) { + return; + } + + initRepoBranchTagSelector('.js-branch-tag-selector'); + + // Options + if ($('.repository.settings.options').length > 0) { + // Enable or select internal/external wiki system and issue tracker. + $('.enable-system').on('change', function () { + if (this.checked) { + $($(this).data('target')).removeClass('disabled'); + if (!$(this).data('context')) $($(this).data('context')).addClass('disabled'); + } else { + $($(this).data('target')).addClass('disabled'); + if (!$(this).data('context')) $($(this).data('context')).removeClass('disabled'); + } + }); + $('.enable-system-radio').on('change', function () { + if (this.value === 'false') { + $($(this).data('target')).addClass('disabled'); + if ($(this).data('context') !== undefined) $($(this).data('context')).removeClass('disabled'); + } else if (this.value === 'true') { + $($(this).data('target')).removeClass('disabled'); + if ($(this).data('context') !== undefined) $($(this).data('context')).addClass('disabled'); + } + }); + const $trackerIssueStyleRadios = $('.js-tracker-issue-style'); + $trackerIssueStyleRadios.on('change input', () => { + const checkedVal = $trackerIssueStyleRadios.filter(':checked').val(); + $('#tracker-issue-style-regex-box').toggleClass('disabled', checkedVal !== 'regexp'); + }); + } + + // Labels + initCompLabelEdit('.repository.labels'); + + // Milestones + if ($('.repository.new.milestone').length > 0) { + $('#clear-date').on('click', () => { + $('#deadline').val(''); + return false; + }); + } + + // Repo Creation + if ($('.repository.new.repo').length > 0) { + $('input[name="gitignores"], input[name="license"]').on('change', () => { + const gitignores = $('input[name="gitignores"]').val(); + const license = $('input[name="license"]').val(); + if (gitignores || license) { + $('input[name="auto_init"]').prop('checked', true); + } + }); + } + + // Compare or pull request + const $repoDiff = $('.repository.diff'); + if ($repoDiff.length) { + initRepoCommonBranchOrTagDropdown('.choose.branch .dropdown'); + initRepoCommonFilterSearchDropdown('.choose.branch .dropdown'); + } + + initRepoCloneLink(); + initCitationFileCopyContent(); + initRepoCommonLanguageStats(); + initRepoSettingBranches(); + + // Issues + if ($('.repository.view.issue').length > 0) { + initRepoIssueCommentEdit(); + + initRepoIssueBranchSelect(); + initRepoIssueTitleEdit(); + initRepoIssueWipToggle(); + initRepoIssueComments(); + + initRepoDiffConversationNav(); + initRepoIssueReferenceIssue(); + + + initRepoIssueCommentDelete(); + initRepoIssueDependencyDelete(); + initRepoIssueCodeCommentCancel(); + initRepoPullRequestUpdate(); + initCompReactionSelector($(document)); + + initRepoPullRequestMergeForm(); + } + + // Pull request + const $repoComparePull = $('.repository.compare.pull'); + if ($repoComparePull.length > 0) { + // show pull request form + $repoComparePull.find('button.show-form').on('click', function (e) { + e.preventDefault(); + hideElem($(this).parent()); + + const $form = $repoComparePull.find('.pullrequest-form'); + showElem($form); + }); + } + + initUnicodeEscapeButton(); +} + +function initRepoIssueCommentEdit() { + // Edit issue or comment content + $(document).on('click', '.edit-content', onEditContent); + + // Quote reply + $(document).on('click', '.quote-reply', async function (event) { + event.preventDefault(); + const target = $(this).data('target'); + const quote = $(`#${target}`).text().replace(/\n/g, '\n> '); + const content = `> ${quote}\n\n`; + let editor; + if ($(this).hasClass('quote-reply-diff')) { + const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply'); + editor = await handleReply($replyBtn); + } else { + // for normal issue/comment page + editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor')); + } + if (editor) { + if (editor.value()) { + editor.value(`${editor.value()}\n\n${content}`); + } else { + editor.value(content); + } + editor.focus(); + editor.moveCursorToEnd(); + } + }); +} From 5929b87112406376038107c1fcb747d9f4d42c07 Mon Sep 17 00:00:00 2001 From: splitt3r Date: Sat, 19 Aug 2023 10:49:01 +0000 Subject: [PATCH 02/26] Add translatanle clear reviewers label --- options/locale/locale_en-US.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 06bf57fc62cf2..7ad56112098de 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1458,10 +1458,11 @@ issues.new.closed_milestone = Closed Milestones issues.new.assignees = Assignees issues.new.clear_assignees = Clear assignees issues.new.no_assignees = No Assignees -issues.new.no_reviewers = No reviewers +issues.new.no_reviewers = No Reviewers issues.new.blocked_user = Cannot create issue because you are blocked by the repository owner. issues.edit.already_changed = Unable to save changes to the issue. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes issues.edit.blocked_user = Cannot edit content because you are blocked by the poster or repository owner. +issues.new.clear_reviewers = Clear reviewers issues.choose.get_started = Get Started issues.choose.open_external_link = Open issues.choose.blank = Default From 452828abbdee13e2a068697eb5105931fa5554f6 Mon Sep 17 00:00:00 2001 From: splitt3r Date: Sat, 19 Aug 2023 12:29:09 +0000 Subject: [PATCH 03/26] Add missing continue statement --- services/pull/pull.go | 1 + 1 file changed, 1 insertion(+) diff --git a/services/pull/pull.go b/services/pull/pull.go index 68e81814deb56..5a308dea2bbb0 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -127,6 +127,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss if err != nil { return err } + continue } reviewer, err := user_model.GetUserByID(ctx, reviewerID) From 906514e1cb93e5895ba5dd15e9622e924bf84399 Mon Sep 17 00:00:00 2001 From: splitt3r Date: Fri, 1 Sep 2023 10:57:52 +0000 Subject: [PATCH 04/26] Check pr creator permissions --- routers/web/repo/issue.go | 3 ++- routers/web/repo/pull.go | 2 +- services/pull/pull.go | 15 ++++++++++++++- templates/repo/issue/new_form.tmpl | 6 +++++- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 136dd66e5ad4f..02ee60d0fd2a1 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1225,8 +1225,9 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull return nil, nil, nil, 0, 0 } - // Check if the passed reviewers actually exist + // Check if the passed reviewers (user/team) actually exist for _, rID := range reviewerIDs { + // negative reviewIDs represent team requests if rID < 0 { _, err := organization.GetTeamByID(ctx, -rID) if err != nil { diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 05b47faeff130..a56014efb6a12 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1268,7 +1268,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { return } - labelIDs, assigneeIDs, reviewerIDs, milestoneID, _ := ValidateRepoMetas(ctx, *form, true) + labelIDs, assigneeIDs, reviewerIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, true) if ctx.Written() { return } diff --git a/services/pull/pull.go b/services/pull/pull.go index 5a308dea2bbb0..db2c23a2fe2b0 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -43,7 +43,7 @@ func getPullWorkingLockKey(prID int64) string { } // NewPullRequest creates new pull request with labels for repository. -func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64, reviewerIDs []int64) error { +func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs, reviewerIDs []int64) error { if err := issue.LoadPoster(ctx); err != nil { return err } @@ -118,11 +118,16 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss } for _, reviewerID := range reviewerIDs { + // negative reviewIDs represent team requests if reviewerID < 0 { team, err := organization.GetTeamByID(ctx, -reviewerID) if err != nil { return err } + err = issue_service.IsValidTeamReviewRequest(ctx, team, issue.Poster, true, issue) + if err != nil { + return err + } _, err = issue_service.TeamReviewRequest(ctx, issue, issue.Poster, team, true) if err != nil { return err @@ -134,6 +139,14 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss if err != nil { return err } + permDoer, err := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) + if err != nil { + return err + } + err = issue_service.IsValidReviewRequest(ctx, reviewer, issue.Poster, true, issue, &permDoer) + if err != nil { + return err + } _, err = issue_service.ReviewRequest(ctx, issue, issue.Poster, reviewer, true) if err != nil { return err diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index db7a44fec3c09..070e9e6761bfa 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -69,7 +69,11 @@ {{ctx.AvatarUtils.Avatar .User 28 "gt-mr-3"}}{{template "repo/search_name" .User}} - {{else if .Team}} + {{end}} + {{end}} +
+ {{range .Reviewers}} + {{if .Team}} From 45b8a965a2139ed79396f095191617cece39857f Mon Sep 17 00:00:00 2001 From: splitt3r Date: Fri, 1 Sep 2023 11:07:05 +0000 Subject: [PATCH 05/26] Remove unintended change --- services/pull/pull.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/pull/pull.go b/services/pull/pull.go index db2c23a2fe2b0..4a267d39acd12 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -43,7 +43,7 @@ func getPullWorkingLockKey(prID int64) string { } // NewPullRequest creates new pull request with labels for repository. -func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs, reviewerIDs []int64) error { +func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64, reviewerIDs []int64) error { if err := issue.LoadPoster(ctx); err != nil { return err } From 3e0dceac77e4f3cee78c7596121a79bab0af49f9 Mon Sep 17 00:00:00 2001 From: splitt3r Date: Sat, 9 Sep 2023 10:28:05 +0000 Subject: [PATCH 06/26] Check if Pull + run linter --- routers/web/repo/issue.go | 38 ++++++++++++++++++++------------------ services/pull/pull.go | 2 +- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 02ee60d0fd2a1..49e93f7de3825 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1219,28 +1219,30 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull // Check reviewers var reviewerIDs []int64 - if len(form.ReviewerIDs) > 0 { - reviewerIDs, err = base.StringsToInt64s(strings.Split(form.ReviewerIDs, ",")) - if err != nil { - return nil, nil, nil, 0, 0 - } + if isPull { + if len(form.ReviewerIDs) > 0 { + reviewerIDs, err = base.StringsToInt64s(strings.Split(form.ReviewerIDs, ",")) + if err != nil { + return nil, nil, nil, 0, 0 + } - // Check if the passed reviewers (user/team) actually exist - for _, rID := range reviewerIDs { - // negative reviewIDs represent team requests - if rID < 0 { - _, err := organization.GetTeamByID(ctx, -rID) + // Check if the passed reviewers (user/team) actually exist + for _, rID := range reviewerIDs { + // negative reviewIDs represent team requests + if rID < 0 { + _, err := organization.GetTeamByID(ctx, -rID) + if err != nil { + ctx.ServerError("GetTeamByID", err) + return nil, nil, nil, 0, 0 + } + continue + } + + _, err := user_model.GetUserByID(ctx, rID) if err != nil { - ctx.ServerError("GetTeamByID", err) + ctx.ServerError("GetUserByID", err) return nil, nil, nil, 0, 0 } - continue - } - - _, err := user_model.GetUserByID(ctx, rID) - if err != nil { - ctx.ServerError("GetUserByID", err) - return nil, nil, nil, 0, 0 } } } diff --git a/services/pull/pull.go b/services/pull/pull.go index 4a267d39acd12..db2c23a2fe2b0 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -43,7 +43,7 @@ func getPullWorkingLockKey(prID int64) string { } // NewPullRequest creates new pull request with labels for repository. -func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64, reviewerIDs []int64) error { +func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs, reviewerIDs []int64) error { if err := issue.LoadPoster(ctx); err != nil { return err } From 978c465dd2f03b42db4d2fd6393c643797ab8430 Mon Sep 17 00:00:00 2001 From: Sebastian Sauer Date: Fri, 20 Oct 2023 22:04:40 +0200 Subject: [PATCH 07/26] Fix invisible class name --- templates/repo/issue/new_form.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index 070e9e6761bfa..6ac7c16088704 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -64,7 +64,7 @@ {{range .Reviewers}} {{if .User}} - + {{svg "octicon-check"}} {{ctx.AvatarUtils.Avatar .User 28 "gt-mr-3"}}{{template "repo/search_name" .User}} @@ -75,7 +75,7 @@ {{range .Reviewers}} {{if .Team}} - + {{svg "octicon-check" 16}} {{svg "octicon-people" 16 "gt-ml-4 gt-mr-2"}}{{$.Repository.OwnerName}}/{{.Team.Name}} From c92202e36317d9f55618b5558414e1bdac8aa63b Mon Sep 17 00:00:00 2001 From: Sebastian Sauer Date: Fri, 20 Oct 2023 22:17:45 +0200 Subject: [PATCH 08/26] Add request review to API --- modules/structs/pull.go | 4 +++- routers/api/v1/repo/pull.go | 41 +++++++++++++++++++++++++++++++++- templates/swagger/v1_json.tmpl | 14 ++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/modules/structs/pull.go b/modules/structs/pull.go index ab627666c94af..55831e642c1a0 100644 --- a/modules/structs/pull.go +++ b/modules/structs/pull.go @@ -86,7 +86,9 @@ type CreatePullRequestOption struct { Milestone int64 `json:"milestone"` Labels []int64 `json:"labels"` // swagger:strfmt date-time - Deadline *time.Time `json:"due_date"` + Deadline *time.Time `json:"due_date"` + Reviewers []string `json:"reviewers"` + TeamReviewers []string `json:"team_reviewers"` } // EditPullRequestOption options when modify pull request diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 0f98507a7eb53..0732703f12313 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -16,6 +16,7 @@ import ( activities_model "code.gitea.io/gitea/models/activities" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" @@ -553,8 +554,46 @@ func CreatePullRequest(ctx *context.APIContext) { return } } + // handle reviewers + var reviewerIds []int64 - if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs, []int64{}); err != nil { + for _, r := range form.Reviewers { + var reviewer *user_model.User + if strings.Contains(r, "@") { + reviewer, err = user_model.GetUserByEmail(ctx, r) + } else { + reviewer, err = user_model.GetUserByName(ctx, r) + } + + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.NotFound("UserNotExist", fmt.Sprintf("User with id '%s' not exist", r)) + return + } + ctx.Error(http.StatusInternalServerError, "GetUser", err) + return + } + reviewerIds = append(reviewerIds, reviewer.ID) + } + + // handle teams as reviewers + if ctx.Repo.Repository.Owner.IsOrganization() && len(form.TeamReviewers) > 0 { + for _, t := range form.TeamReviewers { + var teamReviewer *organization.Team + teamReviewer, err = organization.GetTeam(ctx, ctx.Repo.Owner.ID, t) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.NotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t)) + return + } + ctx.Error(http.StatusInternalServerError, "ReviewRequest", err) + return + } + reviewerIds = append(reviewerIds, teamReviewer.ID) + } + } + + if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs, reviewerIds); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err) } else if errors.Is(err, user_model.ErrBlockedUser) { diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 4cbf511aacbe8..0c898eb672d9c 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -20086,6 +20086,20 @@ "format": "int64", "x-go-name": "Milestone" }, + "reviewers": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "Reviewers" + }, + "team_reviewers": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "TeamReviewers" + }, "title": { "type": "string", "x-go-name": "Title" From df3f36394a5f2a6f86e447e3a8a7f6fc99845f47 Mon Sep 17 00:00:00 2001 From: Sebastian Sauer Date: Wed, 15 Nov 2023 21:32:15 +0100 Subject: [PATCH 09/26] Fix call to locale in tmpl --- templates/repo/issue/new_form.tmpl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index 6ac7c16088704..adbe9e1e9e8ed 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -52,15 +52,15 @@