code.gitea.io/gitea@v1.21.7/routers/web/repo/issue.go (about) 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 // Copyright 2018 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package repo 6 7 import ( 8 "bytes" 9 stdCtx "context" 10 "errors" 11 "fmt" 12 "math/big" 13 "net/http" 14 "net/url" 15 "slices" 16 "sort" 17 "strconv" 18 "strings" 19 "time" 20 21 activities_model "code.gitea.io/gitea/models/activities" 22 "code.gitea.io/gitea/models/db" 23 git_model "code.gitea.io/gitea/models/git" 24 issues_model "code.gitea.io/gitea/models/issues" 25 "code.gitea.io/gitea/models/organization" 26 access_model "code.gitea.io/gitea/models/perm/access" 27 project_model "code.gitea.io/gitea/models/project" 28 pull_model "code.gitea.io/gitea/models/pull" 29 repo_model "code.gitea.io/gitea/models/repo" 30 "code.gitea.io/gitea/models/unit" 31 user_model "code.gitea.io/gitea/models/user" 32 "code.gitea.io/gitea/modules/base" 33 "code.gitea.io/gitea/modules/container" 34 "code.gitea.io/gitea/modules/context" 35 "code.gitea.io/gitea/modules/git" 36 issue_indexer "code.gitea.io/gitea/modules/indexer/issues" 37 issue_template "code.gitea.io/gitea/modules/issue/template" 38 "code.gitea.io/gitea/modules/log" 39 "code.gitea.io/gitea/modules/markup" 40 "code.gitea.io/gitea/modules/markup/markdown" 41 repo_module "code.gitea.io/gitea/modules/repository" 42 "code.gitea.io/gitea/modules/setting" 43 api "code.gitea.io/gitea/modules/structs" 44 "code.gitea.io/gitea/modules/templates/vars" 45 "code.gitea.io/gitea/modules/timeutil" 46 "code.gitea.io/gitea/modules/upload" 47 "code.gitea.io/gitea/modules/util" 48 "code.gitea.io/gitea/modules/web" 49 "code.gitea.io/gitea/routers/utils" 50 asymkey_service "code.gitea.io/gitea/services/asymkey" 51 "code.gitea.io/gitea/services/convert" 52 "code.gitea.io/gitea/services/forms" 53 issue_service "code.gitea.io/gitea/services/issue" 54 pull_service "code.gitea.io/gitea/services/pull" 55 repo_service "code.gitea.io/gitea/services/repository" 56 ) 57 58 const ( 59 tplAttachment base.TplName = "repo/issue/view_content/attachments" 60 61 tplIssues base.TplName = "repo/issue/list" 62 tplIssueNew base.TplName = "repo/issue/new" 63 tplIssueChoose base.TplName = "repo/issue/choose" 64 tplIssueView base.TplName = "repo/issue/view" 65 66 tplReactions base.TplName = "repo/issue/view_content/reactions" 67 68 issueTemplateKey = "IssueTemplate" 69 issueTemplateTitleKey = "IssueTemplateTitle" 70 ) 71 72 // IssueTemplateCandidates issue templates 73 var IssueTemplateCandidates = []string{ 74 "ISSUE_TEMPLATE.md", 75 "ISSUE_TEMPLATE.yaml", 76 "ISSUE_TEMPLATE.yml", 77 "issue_template.md", 78 "issue_template.yaml", 79 "issue_template.yml", 80 ".gitea/ISSUE_TEMPLATE.md", 81 ".gitea/ISSUE_TEMPLATE.yaml", 82 ".gitea/ISSUE_TEMPLATE.yml", 83 ".gitea/issue_template.md", 84 ".gitea/issue_template.yaml", 85 ".gitea/issue_template.yml", 86 ".github/ISSUE_TEMPLATE.md", 87 ".github/ISSUE_TEMPLATE.yaml", 88 ".github/ISSUE_TEMPLATE.yml", 89 ".github/issue_template.md", 90 ".github/issue_template.yaml", 91 ".github/issue_template.yml", 92 } 93 94 // MustAllowUserComment checks to make sure if an issue is locked. 95 // If locked and user has permissions to write to the repository, 96 // then the comment is allowed, else it is blocked 97 func MustAllowUserComment(ctx *context.Context) { 98 issue := GetActionIssue(ctx) 99 if ctx.Written() { 100 return 101 } 102 103 if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { 104 ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked")) 105 ctx.Redirect(issue.Link()) 106 return 107 } 108 } 109 110 // MustEnableIssues check if repository enable internal issues 111 func MustEnableIssues(ctx *context.Context) { 112 if !ctx.Repo.CanRead(unit.TypeIssues) && 113 !ctx.Repo.CanRead(unit.TypeExternalTracker) { 114 ctx.NotFound("MustEnableIssues", nil) 115 return 116 } 117 118 unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker) 119 if err == nil { 120 ctx.Redirect(unit.ExternalTrackerConfig().ExternalTrackerURL) 121 return 122 } 123 } 124 125 // MustAllowPulls check if repository enable pull requests and user have right to do that 126 func MustAllowPulls(ctx *context.Context) { 127 if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) { 128 ctx.NotFound("MustAllowPulls", nil) 129 return 130 } 131 132 // User can send pull request if owns a forked repository. 133 if ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) { 134 ctx.Repo.PullRequest.Allowed = true 135 ctx.Repo.PullRequest.HeadInfoSubURL = url.PathEscape(ctx.Doer.Name) + ":" + util.PathEscapeSegments(ctx.Repo.BranchName) 136 } 137 } 138 139 func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) { 140 var err error 141 viewType := ctx.FormString("type") 142 sortType := ctx.FormString("sort") 143 types := []string{"all", "your_repositories", "assigned", "created_by", "mentioned", "review_requested", "reviewed_by"} 144 if !util.SliceContainsString(types, viewType, true) { 145 viewType = "all" 146 } 147 148 var ( 149 assigneeID = ctx.FormInt64("assignee") 150 posterID = ctx.FormInt64("poster") 151 mentionedID int64 152 reviewRequestedID int64 153 reviewedID int64 154 ) 155 156 if ctx.IsSigned { 157 switch viewType { 158 case "created_by": 159 posterID = ctx.Doer.ID 160 case "mentioned": 161 mentionedID = ctx.Doer.ID 162 case "assigned": 163 assigneeID = ctx.Doer.ID 164 case "review_requested": 165 reviewRequestedID = ctx.Doer.ID 166 case "reviewed_by": 167 reviewedID = ctx.Doer.ID 168 } 169 } 170 171 repo := ctx.Repo.Repository 172 var labelIDs []int64 173 // 1,-2 means including label 1 and excluding label 2 174 // 0 means issues with no label 175 // blank means labels will not be filtered for issues 176 selectLabels := ctx.FormString("labels") 177 if selectLabels == "" { 178 ctx.Data["AllLabels"] = true 179 } else if selectLabels == "0" { 180 ctx.Data["NoLabel"] = true 181 } 182 if len(selectLabels) > 0 { 183 labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ",")) 184 if err != nil { 185 ctx.ServerError("StringsToInt64s", err) 186 return 187 } 188 } 189 190 keyword := strings.Trim(ctx.FormString("q"), " ") 191 if bytes.Contains([]byte(keyword), []byte{0x00}) { 192 keyword = "" 193 } 194 195 var mileIDs []int64 196 if milestoneID > 0 || milestoneID == db.NoConditionID { // -1 to get those issues which have no any milestone assigned 197 mileIDs = []int64{milestoneID} 198 } 199 200 var issueStats *issues_model.IssueStats 201 { 202 statsOpts := &issues_model.IssuesOptions{ 203 RepoIDs: []int64{repo.ID}, 204 LabelIDs: labelIDs, 205 MilestoneIDs: mileIDs, 206 ProjectID: projectID, 207 AssigneeID: assigneeID, 208 MentionedID: mentionedID, 209 PosterID: posterID, 210 ReviewRequestedID: reviewRequestedID, 211 ReviewedID: reviewedID, 212 IsPull: isPullOption, 213 IssueIDs: nil, 214 } 215 if keyword != "" { 216 allIssueIDs, err := issueIDsFromSearch(ctx, keyword, statsOpts) 217 if err != nil { 218 if issue_indexer.IsAvailable(ctx) { 219 ctx.ServerError("issueIDsFromSearch", err) 220 return 221 } 222 ctx.Data["IssueIndexerUnavailable"] = true 223 return 224 } 225 statsOpts.IssueIDs = allIssueIDs 226 } 227 if keyword != "" && len(statsOpts.IssueIDs) == 0 { 228 // So it did search with the keyword, but no issue found. 229 // Just set issueStats to empty. 230 issueStats = &issues_model.IssueStats{} 231 } else { 232 // So it did search with the keyword, and found some issues. It needs to get issueStats of these issues. 233 // Or the keyword is empty, so it doesn't need issueIDs as filter, just get issueStats with statsOpts. 234 issueStats, err = issues_model.GetIssueStats(ctx, statsOpts) 235 if err != nil { 236 ctx.ServerError("GetIssueStats", err) 237 return 238 } 239 } 240 241 } 242 243 isShowClosed := ctx.FormString("state") == "closed" 244 // if open issues are zero and close don't, use closed as default 245 if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 { 246 isShowClosed = true 247 } 248 249 page := ctx.FormInt("page") 250 if page <= 1 { 251 page = 1 252 } 253 254 var total int 255 if !isShowClosed { 256 total = int(issueStats.OpenCount) 257 } else { 258 total = int(issueStats.ClosedCount) 259 } 260 pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5) 261 262 var issues issues_model.IssueList 263 { 264 ids, err := issueIDsFromSearch(ctx, keyword, &issues_model.IssuesOptions{ 265 Paginator: &db.ListOptions{ 266 Page: pager.Paginater.Current(), 267 PageSize: setting.UI.IssuePagingNum, 268 }, 269 RepoIDs: []int64{repo.ID}, 270 AssigneeID: assigneeID, 271 PosterID: posterID, 272 MentionedID: mentionedID, 273 ReviewRequestedID: reviewRequestedID, 274 ReviewedID: reviewedID, 275 MilestoneIDs: mileIDs, 276 ProjectID: projectID, 277 IsClosed: util.OptionalBoolOf(isShowClosed), 278 IsPull: isPullOption, 279 LabelIDs: labelIDs, 280 SortType: sortType, 281 }) 282 if err != nil { 283 if issue_indexer.IsAvailable(ctx) { 284 ctx.ServerError("issueIDsFromSearch", err) 285 return 286 } 287 ctx.Data["IssueIndexerUnavailable"] = true 288 return 289 } 290 issues, err = issues_model.GetIssuesByIDs(ctx, ids, true) 291 if err != nil { 292 ctx.ServerError("GetIssuesByIDs", err) 293 return 294 } 295 } 296 297 approvalCounts, err := issues.GetApprovalCounts(ctx) 298 if err != nil { 299 ctx.ServerError("ApprovalCounts", err) 300 return 301 } 302 303 // Get posters. 304 for i := range issues { 305 // Check read status 306 if !ctx.IsSigned { 307 issues[i].IsRead = true 308 } else if err = issues[i].GetIsRead(ctx.Doer.ID); err != nil { 309 ctx.ServerError("GetIsRead", err) 310 return 311 } 312 } 313 314 commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues) 315 if err != nil { 316 ctx.ServerError("GetIssuesAllCommitStatus", err) 317 return 318 } 319 320 if err := issues.LoadAttributes(ctx); err != nil { 321 ctx.ServerError("issues.LoadAttributes", err) 322 return 323 } 324 325 ctx.Data["Issues"] = issues 326 ctx.Data["CommitLastStatus"] = lastStatus 327 ctx.Data["CommitStatuses"] = commitStatuses 328 329 // Get assignees. 330 assigneeUsers, err := repo_model.GetRepoAssignees(ctx, repo) 331 if err != nil { 332 ctx.ServerError("GetRepoAssignees", err) 333 return 334 } 335 ctx.Data["Assignees"] = MakeSelfOnTop(ctx.Doer, assigneeUsers) 336 337 handleTeamMentions(ctx) 338 if ctx.Written() { 339 return 340 } 341 342 labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{}) 343 if err != nil { 344 ctx.ServerError("GetLabelsByRepoID", err) 345 return 346 } 347 348 if repo.Owner.IsOrganization() { 349 orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) 350 if err != nil { 351 ctx.ServerError("GetLabelsByOrgID", err) 352 return 353 } 354 355 ctx.Data["OrgLabels"] = orgLabels 356 labels = append(labels, orgLabels...) 357 } 358 359 // Get the exclusive scope for every label ID 360 labelExclusiveScopes := make([]string, 0, len(labelIDs)) 361 for _, labelID := range labelIDs { 362 foundExclusiveScope := false 363 for _, label := range labels { 364 if label.ID == labelID || label.ID == -labelID { 365 labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope()) 366 foundExclusiveScope = true 367 break 368 } 369 } 370 if !foundExclusiveScope { 371 labelExclusiveScopes = append(labelExclusiveScopes, "") 372 } 373 } 374 375 for _, l := range labels { 376 l.LoadSelectedLabelsAfterClick(labelIDs, labelExclusiveScopes) 377 } 378 ctx.Data["Labels"] = labels 379 ctx.Data["NumLabels"] = len(labels) 380 381 if ctx.FormInt64("assignee") == 0 { 382 assigneeID = 0 // Reset ID to prevent unexpected selection of assignee. 383 } 384 385 ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink) 386 387 ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 { 388 counts, ok := approvalCounts[issueID] 389 if !ok || len(counts) == 0 { 390 return 0 391 } 392 reviewTyp := issues_model.ReviewTypeApprove 393 if typ == "reject" { 394 reviewTyp = issues_model.ReviewTypeReject 395 } else if typ == "waiting" { 396 reviewTyp = issues_model.ReviewTypeRequest 397 } 398 for _, count := range counts { 399 if count.Type == reviewTyp { 400 return count.Count 401 } 402 } 403 return 0 404 } 405 406 retrieveProjects(ctx, repo) 407 if ctx.Written() { 408 return 409 } 410 411 pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.IsTrue()) 412 if err != nil { 413 ctx.ServerError("GetPinnedIssues", err) 414 return 415 } 416 417 ctx.Data["PinnedIssues"] = pinned 418 ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin) 419 ctx.Data["IssueStats"] = issueStats 420 ctx.Data["SelLabelIDs"] = labelIDs 421 ctx.Data["SelectLabels"] = selectLabels 422 ctx.Data["ViewType"] = viewType 423 ctx.Data["SortType"] = sortType 424 ctx.Data["MilestoneID"] = milestoneID 425 ctx.Data["ProjectID"] = projectID 426 ctx.Data["AssigneeID"] = assigneeID 427 ctx.Data["PosterID"] = posterID 428 ctx.Data["IsShowClosed"] = isShowClosed 429 ctx.Data["Keyword"] = keyword 430 if isShowClosed { 431 ctx.Data["State"] = "closed" 432 } else { 433 ctx.Data["State"] = "open" 434 } 435 436 pager.AddParam(ctx, "q", "Keyword") 437 pager.AddParam(ctx, "type", "ViewType") 438 pager.AddParam(ctx, "sort", "SortType") 439 pager.AddParam(ctx, "state", "State") 440 pager.AddParam(ctx, "labels", "SelectLabels") 441 pager.AddParam(ctx, "milestone", "MilestoneID") 442 pager.AddParam(ctx, "project", "ProjectID") 443 pager.AddParam(ctx, "assignee", "AssigneeID") 444 pager.AddParam(ctx, "poster", "PosterID") 445 446 if ctx.FormBool("archived") { 447 ctx.Data["ShowArchivedLabels"] = true 448 pager.AddParam(ctx, "archived", "ShowArchivedLabels") 449 } 450 ctx.Data["Page"] = pager 451 } 452 453 func issueIDsFromSearch(ctx *context.Context, keyword string, opts *issues_model.IssuesOptions) ([]int64, error) { 454 ids, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts)) 455 if err != nil { 456 return nil, fmt.Errorf("SearchIssues: %w", err) 457 } 458 return ids, nil 459 } 460 461 // Issues render issues page 462 func Issues(ctx *context.Context) { 463 isPullList := ctx.Params(":type") == "pulls" 464 if isPullList { 465 MustAllowPulls(ctx) 466 if ctx.Written() { 467 return 468 } 469 ctx.Data["Title"] = ctx.Tr("repo.pulls") 470 ctx.Data["PageIsPullList"] = true 471 } else { 472 MustEnableIssues(ctx) 473 if ctx.Written() { 474 return 475 } 476 ctx.Data["Title"] = ctx.Tr("repo.issues") 477 ctx.Data["PageIsIssueList"] = true 478 ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) 479 } 480 481 issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList)) 482 if ctx.Written() { 483 return 484 } 485 486 renderMilestones(ctx) 487 if ctx.Written() { 488 return 489 } 490 491 ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.CanWriteIssuesOrPulls(isPullList) 492 493 ctx.HTML(http.StatusOK, tplIssues) 494 } 495 496 func renderMilestones(ctx *context.Context) { 497 // Get milestones 498 milestones, _, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{ 499 RepoID: ctx.Repo.Repository.ID, 500 State: api.StateAll, 501 }) 502 if err != nil { 503 ctx.ServerError("GetAllRepoMilestones", err) 504 return 505 } 506 507 openMilestones, closedMilestones := issues_model.MilestoneList{}, issues_model.MilestoneList{} 508 for _, milestone := range milestones { 509 if milestone.IsClosed { 510 closedMilestones = append(closedMilestones, milestone) 511 } else { 512 openMilestones = append(openMilestones, milestone) 513 } 514 } 515 ctx.Data["OpenMilestones"] = openMilestones 516 ctx.Data["ClosedMilestones"] = closedMilestones 517 } 518 519 // RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository 520 func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.Repository) { 521 var err error 522 ctx.Data["OpenMilestones"], _, err = issues_model.GetMilestones(issues_model.GetMilestonesOption{ 523 RepoID: repo.ID, 524 State: api.StateOpen, 525 }) 526 if err != nil { 527 ctx.ServerError("GetMilestones", err) 528 return 529 } 530 ctx.Data["ClosedMilestones"], _, err = issues_model.GetMilestones(issues_model.GetMilestonesOption{ 531 RepoID: repo.ID, 532 State: api.StateClosed, 533 }) 534 if err != nil { 535 ctx.ServerError("GetMilestones", err) 536 return 537 } 538 539 assigneeUsers, err := repo_model.GetRepoAssignees(ctx, repo) 540 if err != nil { 541 ctx.ServerError("GetRepoAssignees", err) 542 return 543 } 544 ctx.Data["Assignees"] = MakeSelfOnTop(ctx.Doer, assigneeUsers) 545 546 handleTeamMentions(ctx) 547 } 548 549 func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { 550 // Distinguish whether the owner of the repository 551 // is an individual or an organization 552 repoOwnerType := project_model.TypeIndividual 553 if repo.Owner.IsOrganization() { 554 repoOwnerType = project_model.TypeOrganization 555 } 556 var err error 557 projects, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{ 558 RepoID: repo.ID, 559 Page: -1, 560 IsClosed: util.OptionalBoolFalse, 561 Type: project_model.TypeRepository, 562 }) 563 if err != nil { 564 ctx.ServerError("GetProjects", err) 565 return 566 } 567 projects2, _, err := project_model.FindProjects(ctx, project_model.SearchOptions{ 568 OwnerID: repo.OwnerID, 569 Page: -1, 570 IsClosed: util.OptionalBoolFalse, 571 Type: repoOwnerType, 572 }) 573 if err != nil { 574 ctx.ServerError("GetProjects", err) 575 return 576 } 577 578 ctx.Data["OpenProjects"] = append(projects, projects2...) 579 580 projects, _, err = project_model.FindProjects(ctx, project_model.SearchOptions{ 581 RepoID: repo.ID, 582 Page: -1, 583 IsClosed: util.OptionalBoolTrue, 584 Type: project_model.TypeRepository, 585 }) 586 if err != nil { 587 ctx.ServerError("GetProjects", err) 588 return 589 } 590 projects2, _, err = project_model.FindProjects(ctx, project_model.SearchOptions{ 591 OwnerID: repo.OwnerID, 592 Page: -1, 593 IsClosed: util.OptionalBoolTrue, 594 Type: repoOwnerType, 595 }) 596 if err != nil { 597 ctx.ServerError("GetProjects", err) 598 return 599 } 600 601 ctx.Data["ClosedProjects"] = append(projects, projects2...) 602 } 603 604 // repoReviewerSelection items to bee shown 605 type repoReviewerSelection struct { 606 IsTeam bool 607 Team *organization.Team 608 User *user_model.User 609 Review *issues_model.Review 610 CanChange bool 611 Checked bool 612 ItemID int64 613 } 614 615 // RetrieveRepoReviewers find all reviewers of a repository 616 func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, issue *issues_model.Issue, canChooseReviewer bool) { 617 ctx.Data["CanChooseReviewer"] = canChooseReviewer 618 619 originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(ctx, issue.ID) 620 if err != nil { 621 ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err) 622 return 623 } 624 ctx.Data["OriginalReviews"] = originalAuthorReviews 625 626 reviews, err := issues_model.GetReviewsByIssueID(ctx, issue.ID) 627 if err != nil { 628 ctx.ServerError("GetReviewersByIssueID", err) 629 return 630 } 631 632 if len(reviews) == 0 && !canChooseReviewer { 633 return 634 } 635 636 var ( 637 pullReviews []*repoReviewerSelection 638 reviewersResult []*repoReviewerSelection 639 teamReviewersResult []*repoReviewerSelection 640 teamReviewers []*organization.Team 641 reviewers []*user_model.User 642 ) 643 644 if canChooseReviewer { 645 posterID := issue.PosterID 646 if issue.OriginalAuthorID > 0 { 647 posterID = 0 648 } 649 650 reviewers, err = repo_model.GetReviewers(ctx, repo, ctx.Doer.ID, posterID) 651 if err != nil { 652 ctx.ServerError("GetReviewers", err) 653 return 654 } 655 656 teamReviewers, err = repo_service.GetReviewerTeams(ctx, repo) 657 if err != nil { 658 ctx.ServerError("GetReviewerTeams", err) 659 return 660 } 661 662 if len(reviewers) > 0 { 663 reviewersResult = make([]*repoReviewerSelection, 0, len(reviewers)) 664 } 665 666 if len(teamReviewers) > 0 { 667 teamReviewersResult = make([]*repoReviewerSelection, 0, len(teamReviewers)) 668 } 669 } 670 671 pullReviews = make([]*repoReviewerSelection, 0, len(reviews)) 672 673 for _, review := range reviews { 674 tmp := &repoReviewerSelection{ 675 Checked: review.Type == issues_model.ReviewTypeRequest, 676 Review: review, 677 ItemID: review.ReviewerID, 678 } 679 if review.ReviewerTeamID > 0 { 680 tmp.IsTeam = true 681 tmp.ItemID = -review.ReviewerTeamID 682 } 683 684 if ctx.Repo.IsAdmin() { 685 // Admin can dismiss or re-request any review requests 686 tmp.CanChange = true 687 } else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == issues_model.ReviewTypeRequest { 688 // A user can refuse review requests 689 tmp.CanChange = true 690 } else if (canChooseReviewer || (ctx.Doer != nil && ctx.Doer.ID == issue.PosterID)) && review.Type != issues_model.ReviewTypeRequest && 691 ctx.Doer.ID != review.ReviewerID { 692 // The poster of the PR, a manager, or official reviewers can re-request review from other reviewers 693 tmp.CanChange = true 694 } 695 696 pullReviews = append(pullReviews, tmp) 697 698 if canChooseReviewer { 699 if tmp.IsTeam { 700 teamReviewersResult = append(teamReviewersResult, tmp) 701 } else { 702 reviewersResult = append(reviewersResult, tmp) 703 } 704 } 705 } 706 707 if len(pullReviews) > 0 { 708 // Drop all non-existing users and teams from the reviews 709 currentPullReviewers := make([]*repoReviewerSelection, 0, len(pullReviews)) 710 for _, item := range pullReviews { 711 if item.Review.ReviewerID > 0 { 712 if err = item.Review.LoadReviewer(ctx); err != nil { 713 if user_model.IsErrUserNotExist(err) { 714 continue 715 } 716 ctx.ServerError("LoadReviewer", err) 717 return 718 } 719 item.User = item.Review.Reviewer 720 } else if item.Review.ReviewerTeamID > 0 { 721 if err = item.Review.LoadReviewerTeam(ctx); err != nil { 722 if organization.IsErrTeamNotExist(err) { 723 continue 724 } 725 ctx.ServerError("LoadReviewerTeam", err) 726 return 727 } 728 item.Team = item.Review.ReviewerTeam 729 } else { 730 continue 731 } 732 733 currentPullReviewers = append(currentPullReviewers, item) 734 } 735 ctx.Data["PullReviewers"] = currentPullReviewers 736 } 737 738 if canChooseReviewer && reviewersResult != nil { 739 preadded := len(reviewersResult) 740 for _, reviewer := range reviewers { 741 found := false 742 reviewAddLoop: 743 for _, tmp := range reviewersResult[:preadded] { 744 if tmp.ItemID == reviewer.ID { 745 tmp.User = reviewer 746 found = true 747 break reviewAddLoop 748 } 749 } 750 751 if found { 752 continue 753 } 754 755 reviewersResult = append(reviewersResult, &repoReviewerSelection{ 756 IsTeam: false, 757 CanChange: true, 758 User: reviewer, 759 ItemID: reviewer.ID, 760 }) 761 } 762 763 ctx.Data["Reviewers"] = reviewersResult 764 } 765 766 if canChooseReviewer && teamReviewersResult != nil { 767 preadded := len(teamReviewersResult) 768 for _, team := range teamReviewers { 769 found := false 770 teamReviewAddLoop: 771 for _, tmp := range teamReviewersResult[:preadded] { 772 if tmp.ItemID == -team.ID { 773 tmp.Team = team 774 found = true 775 break teamReviewAddLoop 776 } 777 } 778 779 if found { 780 continue 781 } 782 783 teamReviewersResult = append(teamReviewersResult, &repoReviewerSelection{ 784 IsTeam: true, 785 CanChange: true, 786 Team: team, 787 ItemID: -team.ID, 788 }) 789 } 790 791 ctx.Data["TeamReviewers"] = teamReviewersResult 792 } 793 } 794 795 // RetrieveRepoMetas find all the meta information of a repository 796 func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*issues_model.Label { 797 if !ctx.Repo.CanWriteIssuesOrPulls(isPull) { 798 return nil 799 } 800 801 labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{}) 802 if err != nil { 803 ctx.ServerError("GetLabelsByRepoID", err) 804 return nil 805 } 806 ctx.Data["Labels"] = labels 807 if repo.Owner.IsOrganization() { 808 orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) 809 if err != nil { 810 return nil 811 } 812 813 ctx.Data["OrgLabels"] = orgLabels 814 labels = append(labels, orgLabels...) 815 } 816 817 RetrieveRepoMilestonesAndAssignees(ctx, repo) 818 if ctx.Written() { 819 return nil 820 } 821 822 retrieveProjects(ctx, repo) 823 if ctx.Written() { 824 return nil 825 } 826 827 PrepareBranchList(ctx) 828 if ctx.Written() { 829 return nil 830 } 831 832 // Contains true if the user can create issue dependencies 833 ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.Doer, isPull) 834 835 return labels 836 } 837 838 // Tries to load and set an issue template. The first return value indicates if a template was loaded. 839 func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) (bool, map[string]error) { 840 commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) 841 if err != nil { 842 return false, nil 843 } 844 845 templateCandidates := make([]string, 0, 1+len(possibleFiles)) 846 if t := ctx.FormString("template"); t != "" { 847 templateCandidates = append(templateCandidates, t) 848 } 849 templateCandidates = append(templateCandidates, possibleFiles...) // Append files to the end because they should be fallback 850 851 templateErrs := map[string]error{} 852 for _, filename := range templateCandidates { 853 if ok, _ := commit.HasFile(filename); !ok { 854 continue 855 } 856 template, err := issue_template.UnmarshalFromCommit(commit, filename) 857 if err != nil { 858 templateErrs[filename] = err 859 continue 860 } 861 ctx.Data[issueTemplateTitleKey] = template.Title 862 ctx.Data[ctxDataKey] = template.Content 863 864 if template.Type() == api.IssueTemplateTypeYaml { 865 // Replace field default values by values from query 866 for _, field := range template.Fields { 867 fieldValue := ctx.FormString("field:" + field.ID) 868 if fieldValue != "" { 869 field.Attributes["value"] = fieldValue 870 } 871 } 872 873 ctx.Data["Fields"] = template.Fields 874 ctx.Data["TemplateFile"] = template.FileName 875 } 876 labelIDs := make([]string, 0, len(template.Labels)) 877 if repoLabels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil { 878 ctx.Data["Labels"] = repoLabels 879 if ctx.Repo.Owner.IsOrganization() { 880 if orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil { 881 ctx.Data["OrgLabels"] = orgLabels 882 repoLabels = append(repoLabels, orgLabels...) 883 } 884 } 885 886 for _, metaLabel := range template.Labels { 887 for _, repoLabel := range repoLabels { 888 if strings.EqualFold(repoLabel.Name, metaLabel) { 889 repoLabel.IsChecked = true 890 labelIDs = append(labelIDs, strconv.FormatInt(repoLabel.ID, 10)) 891 break 892 } 893 } 894 } 895 896 } 897 898 if template.Ref != "" && !strings.HasPrefix(template.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref> 899 template.Ref = git.BranchPrefix + template.Ref 900 } 901 ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0 902 ctx.Data["label_ids"] = strings.Join(labelIDs, ",") 903 ctx.Data["Reference"] = template.Ref 904 ctx.Data["RefEndName"] = git.RefName(template.Ref).ShortName() 905 return true, templateErrs 906 } 907 return false, templateErrs 908 } 909 910 // NewIssue render creating issue page 911 func NewIssue(ctx *context.Context) { 912 issueConfig, _ := issue_service.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) 913 hasTemplates := issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) 914 915 ctx.Data["Title"] = ctx.Tr("repo.issues.new") 916 ctx.Data["PageIsIssueList"] = true 917 ctx.Data["NewIssueChooseTemplate"] = hasTemplates 918 ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes 919 title := ctx.FormString("title") 920 ctx.Data["TitleQuery"] = title 921 body := ctx.FormString("body") 922 ctx.Data["BodyQuery"] = body 923 924 isProjectsEnabled := ctx.Repo.CanRead(unit.TypeProjects) 925 ctx.Data["IsProjectsEnabled"] = isProjectsEnabled 926 ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled 927 upload.AddUploadContext(ctx, "comment") 928 929 milestoneID := ctx.FormInt64("milestone") 930 if milestoneID > 0 { 931 milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID) 932 if err != nil { 933 log.Error("GetMilestoneByID: %d: %v", milestoneID, err) 934 } else { 935 ctx.Data["milestone_id"] = milestoneID 936 ctx.Data["Milestone"] = milestone 937 } 938 } 939 940 projectID := ctx.FormInt64("project") 941 if projectID > 0 && isProjectsEnabled { 942 project, err := project_model.GetProjectByID(ctx, projectID) 943 if err != nil { 944 log.Error("GetProjectByID: %d: %v", projectID, err) 945 } else if project.RepoID != ctx.Repo.Repository.ID { 946 log.Error("GetProjectByID: %d: %v", projectID, fmt.Errorf("project[%d] not in repo [%d]", project.ID, ctx.Repo.Repository.ID)) 947 } else { 948 ctx.Data["project_id"] = projectID 949 ctx.Data["Project"] = project 950 } 951 952 if len(ctx.Req.URL.Query().Get("project")) > 0 { 953 ctx.Data["redirect_after_creation"] = "project" 954 } 955 } 956 957 RetrieveRepoMetas(ctx, ctx.Repo.Repository, false) 958 959 tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) 960 if err != nil { 961 ctx.ServerError("GetTagNamesByRepoID", err) 962 return 963 } 964 ctx.Data["Tags"] = tags 965 966 ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) 967 templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates) 968 for k, v := range errs { 969 ret.TemplateErrors[k] = v 970 } 971 if ctx.Written() { 972 return 973 } 974 975 if len(ret.TemplateErrors) > 0 { 976 ctx.Flash.Warning(renderErrorOfTemplates(ctx, ret.TemplateErrors), true) 977 } 978 979 ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypeIssues) 980 981 if !issueConfig.BlankIssuesEnabled && hasTemplates && !templateLoaded { 982 // The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if blank issues are disabled, just redirect to the "issues/choose" page with these parameters. 983 ctx.Redirect(fmt.Sprintf("%s/issues/new/choose?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther) 984 return 985 } 986 987 ctx.HTML(http.StatusOK, tplIssueNew) 988 } 989 990 func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string { 991 var files []string 992 for k := range errs { 993 files = append(files, k) 994 } 995 sort.Strings(files) // keep the output stable 996 997 var lines []string 998 for _, file := range files { 999 lines = append(lines, fmt.Sprintf("%s: %v", file, errs[file])) 1000 } 1001 1002 flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{ 1003 "Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"), 1004 "Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)), 1005 "Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")), 1006 }) 1007 if err != nil { 1008 log.Debug("render flash error: %v", err) 1009 flashError = ctx.Tr("repo.issues.choose.ignore_invalid_templates") 1010 } 1011 return flashError 1012 } 1013 1014 // NewIssueChooseTemplate render creating issue from template page 1015 func NewIssueChooseTemplate(ctx *context.Context) { 1016 ctx.Data["Title"] = ctx.Tr("repo.issues.new") 1017 ctx.Data["PageIsIssueList"] = true 1018 1019 ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) 1020 ctx.Data["IssueTemplates"] = ret.IssueTemplates 1021 1022 if len(ret.TemplateErrors) > 0 { 1023 ctx.Flash.Warning(renderErrorOfTemplates(ctx, ret.TemplateErrors), true) 1024 } 1025 1026 if !issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) { 1027 // The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if no template here, just redirect to the "issues/new" page with these parameters. 1028 ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther) 1029 return 1030 } 1031 1032 issueConfig, err := issue_service.GetTemplateConfigFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) 1033 ctx.Data["IssueConfig"] = issueConfig 1034 ctx.Data["IssueConfigError"] = err // ctx.Flash.Err makes problems here 1035 1036 ctx.Data["milestone"] = ctx.FormInt64("milestone") 1037 ctx.Data["project"] = ctx.FormInt64("project") 1038 1039 ctx.HTML(http.StatusOK, tplIssueChoose) 1040 } 1041 1042 // DeleteIssue deletes an issue 1043 func DeleteIssue(ctx *context.Context) { 1044 issue := GetActionIssue(ctx) 1045 if ctx.Written() { 1046 return 1047 } 1048 1049 if err := issue_service.DeleteIssue(ctx, ctx.Doer, ctx.Repo.GitRepo, issue); err != nil { 1050 ctx.ServerError("DeleteIssueByID", err) 1051 return 1052 } 1053 1054 if issue.IsPull { 1055 ctx.Redirect(fmt.Sprintf("%s/pulls", ctx.Repo.Repository.Link()), http.StatusSeeOther) 1056 return 1057 } 1058 1059 ctx.Redirect(fmt.Sprintf("%s/issues", ctx.Repo.Repository.Link()), http.StatusSeeOther) 1060 } 1061 1062 // ValidateRepoMetas check and returns repository's meta information 1063 func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull bool) ([]int64, []int64, int64, int64) { 1064 var ( 1065 repo = ctx.Repo.Repository 1066 err error 1067 ) 1068 1069 labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull) 1070 if ctx.Written() { 1071 return nil, nil, 0, 0 1072 } 1073 1074 var labelIDs []int64 1075 hasSelected := false 1076 // Check labels. 1077 if len(form.LabelIDs) > 0 { 1078 labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ",")) 1079 if err != nil { 1080 return nil, nil, 0, 0 1081 } 1082 labelIDMark := make(container.Set[int64]) 1083 labelIDMark.AddMultiple(labelIDs...) 1084 1085 for i := range labels { 1086 if labelIDMark.Contains(labels[i].ID) { 1087 labels[i].IsChecked = true 1088 hasSelected = true 1089 } 1090 } 1091 } 1092 1093 ctx.Data["Labels"] = labels 1094 ctx.Data["HasSelectedLabel"] = hasSelected 1095 ctx.Data["label_ids"] = form.LabelIDs 1096 1097 // Check milestone. 1098 milestoneID := form.MilestoneID 1099 if milestoneID > 0 { 1100 milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID) 1101 if err != nil { 1102 ctx.ServerError("GetMilestoneByID", err) 1103 return nil, nil, 0, 0 1104 } 1105 if milestone.RepoID != repo.ID { 1106 ctx.ServerError("GetMilestoneByID", err) 1107 return nil, nil, 0, 0 1108 } 1109 ctx.Data["Milestone"] = milestone 1110 ctx.Data["milestone_id"] = milestoneID 1111 } 1112 1113 if form.ProjectID > 0 { 1114 p, err := project_model.GetProjectByID(ctx, form.ProjectID) 1115 if err != nil { 1116 ctx.ServerError("GetProjectByID", err) 1117 return nil, nil, 0, 0 1118 } 1119 if p.RepoID != ctx.Repo.Repository.ID && p.OwnerID != ctx.Repo.Repository.OwnerID { 1120 ctx.NotFound("", nil) 1121 return nil, nil, 0, 0 1122 } 1123 1124 ctx.Data["Project"] = p 1125 ctx.Data["project_id"] = form.ProjectID 1126 } 1127 1128 // Check assignees 1129 var assigneeIDs []int64 1130 if len(form.AssigneeIDs) > 0 { 1131 assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ",")) 1132 if err != nil { 1133 return nil, nil, 0, 0 1134 } 1135 1136 // Check if the passed assignees actually exists and is assignable 1137 for _, aID := range assigneeIDs { 1138 assignee, err := user_model.GetUserByID(ctx, aID) 1139 if err != nil { 1140 ctx.ServerError("GetUserByID", err) 1141 return nil, nil, 0, 0 1142 } 1143 1144 valid, err := access_model.CanBeAssigned(ctx, assignee, repo, isPull) 1145 if err != nil { 1146 ctx.ServerError("CanBeAssigned", err) 1147 return nil, nil, 0, 0 1148 } 1149 1150 if !valid { 1151 ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}) 1152 return nil, nil, 0, 0 1153 } 1154 } 1155 } 1156 1157 // Keep the old assignee id thingy for compatibility reasons 1158 if form.AssigneeID > 0 { 1159 assigneeIDs = append(assigneeIDs, form.AssigneeID) 1160 } 1161 1162 return labelIDs, assigneeIDs, milestoneID, form.ProjectID 1163 } 1164 1165 // NewIssuePost response for creating new issue 1166 func NewIssuePost(ctx *context.Context) { 1167 form := web.GetForm(ctx).(*forms.CreateIssueForm) 1168 ctx.Data["Title"] = ctx.Tr("repo.issues.new") 1169 ctx.Data["PageIsIssueList"] = true 1170 ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) 1171 ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes 1172 ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled 1173 upload.AddUploadContext(ctx, "comment") 1174 1175 var ( 1176 repo = ctx.Repo.Repository 1177 attachments []string 1178 ) 1179 1180 labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, false) 1181 if ctx.Written() { 1182 return 1183 } 1184 1185 if setting.Attachment.Enabled { 1186 attachments = form.Files 1187 } 1188 1189 if ctx.HasError() { 1190 ctx.JSONError(ctx.GetErrMsg()) 1191 return 1192 } 1193 1194 if util.IsEmptyString(form.Title) { 1195 ctx.JSONError(ctx.Tr("repo.issues.new.title_empty")) 1196 return 1197 } 1198 1199 content := form.Content 1200 if filename := ctx.Req.Form.Get("template-file"); filename != "" { 1201 if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil { 1202 content = issue_template.RenderToMarkdown(template, ctx.Req.Form) 1203 } 1204 } 1205 1206 issue := &issues_model.Issue{ 1207 RepoID: repo.ID, 1208 Repo: repo, 1209 Title: form.Title, 1210 PosterID: ctx.Doer.ID, 1211 Poster: ctx.Doer, 1212 MilestoneID: milestoneID, 1213 Content: content, 1214 Ref: form.Ref, 1215 } 1216 1217 if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil { 1218 if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { 1219 ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) 1220 return 1221 } 1222 ctx.ServerError("NewIssue", err) 1223 return 1224 } 1225 1226 if projectID > 0 { 1227 if !ctx.Repo.CanRead(unit.TypeProjects) { 1228 // User must also be able to see the project. 1229 ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects") 1230 return 1231 } 1232 if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil { 1233 ctx.ServerError("ChangeProjectAssign", err) 1234 return 1235 } 1236 } 1237 1238 log.Trace("Issue created: %d/%d", repo.ID, issue.ID) 1239 if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 { 1240 ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10)) 1241 } else { 1242 ctx.JSONRedirect(issue.Link()) 1243 } 1244 } 1245 1246 // roleDescriptor returns the role descriptor for a comment in/with the given repo, poster and issue 1247 func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, issue *issues_model.Issue, hasOriginalAuthor bool) (issues_model.RoleDescriptor, error) { 1248 roleDescriptor := issues_model.RoleDescriptor{} 1249 1250 if hasOriginalAuthor { 1251 return roleDescriptor, nil 1252 } 1253 1254 perm, err := access_model.GetUserRepoPermission(ctx, repo, poster) 1255 if err != nil { 1256 return roleDescriptor, err 1257 } 1258 1259 // If the poster is the actual poster of the issue, enable Poster role. 1260 roleDescriptor.IsPoster = issue.IsPoster(poster.ID) 1261 1262 // Check if the poster is owner of the repo. 1263 if perm.IsOwner() { 1264 // If the poster isn't an admin, enable the owner role. 1265 if !poster.IsAdmin { 1266 roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner 1267 return roleDescriptor, nil 1268 } 1269 1270 // Otherwise check if poster is the real repo admin. 1271 ok, err := access_model.IsUserRealRepoAdmin(repo, poster) 1272 if err != nil { 1273 return roleDescriptor, err 1274 } 1275 if ok { 1276 roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner 1277 return roleDescriptor, nil 1278 } 1279 } 1280 1281 // If repo is organization, check Member role 1282 if err := repo.LoadOwner(ctx); err != nil { 1283 return roleDescriptor, err 1284 } 1285 if repo.Owner.IsOrganization() { 1286 if isMember, err := organization.IsOrganizationMember(ctx, repo.Owner.ID, poster.ID); err != nil { 1287 return roleDescriptor, err 1288 } else if isMember { 1289 roleDescriptor.RoleInRepo = issues_model.RoleRepoMember 1290 return roleDescriptor, nil 1291 } 1292 } 1293 1294 // If the poster is the collaborator of the repo 1295 if isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, poster.ID); err != nil { 1296 return roleDescriptor, err 1297 } else if isCollaborator { 1298 roleDescriptor.RoleInRepo = issues_model.RoleRepoCollaborator 1299 return roleDescriptor, nil 1300 } 1301 1302 hasMergedPR, err := issues_model.HasMergedPullRequestInRepo(ctx, repo.ID, poster.ID) 1303 if err != nil { 1304 return roleDescriptor, err 1305 } else if hasMergedPR { 1306 roleDescriptor.RoleInRepo = issues_model.RoleRepoContributor 1307 } else if issue.IsPull { 1308 // only display first time contributor in the first opening pull request 1309 roleDescriptor.RoleInRepo = issues_model.RoleRepoFirstTimeContributor 1310 } 1311 1312 return roleDescriptor, nil 1313 } 1314 1315 func getBranchData(ctx *context.Context, issue *issues_model.Issue) { 1316 ctx.Data["BaseBranch"] = nil 1317 ctx.Data["HeadBranch"] = nil 1318 ctx.Data["HeadUserName"] = nil 1319 ctx.Data["BaseName"] = ctx.Repo.Repository.OwnerName 1320 if issue.IsPull { 1321 pull := issue.PullRequest 1322 ctx.Data["BaseBranch"] = pull.BaseBranch 1323 ctx.Data["HeadBranch"] = pull.HeadBranch 1324 ctx.Data["HeadUserName"] = pull.MustHeadUserName(ctx) 1325 } 1326 } 1327 1328 // ViewIssue render issue view page 1329 func ViewIssue(ctx *context.Context) { 1330 if ctx.Params(":type") == "issues" { 1331 // If issue was requested we check if repo has external tracker and redirect 1332 extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker) 1333 if err == nil && extIssueUnit != nil { 1334 if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" { 1335 metas := ctx.Repo.Repository.ComposeMetas() 1336 metas["index"] = ctx.Params(":index") 1337 res, err := vars.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas) 1338 if err != nil { 1339 log.Error("unable to expand template vars for issue url. issue: %s, err: %v", metas["index"], err) 1340 ctx.ServerError("Expand", err) 1341 return 1342 } 1343 ctx.Redirect(res) 1344 return 1345 } 1346 } else if err != nil && !repo_model.IsErrUnitTypeNotExist(err) { 1347 ctx.ServerError("GetUnit", err) 1348 return 1349 } 1350 } 1351 1352 issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 1353 if err != nil { 1354 if issues_model.IsErrIssueNotExist(err) { 1355 ctx.NotFound("GetIssueByIndex", err) 1356 } else { 1357 ctx.ServerError("GetIssueByIndex", err) 1358 } 1359 return 1360 } 1361 if issue.Repo == nil { 1362 issue.Repo = ctx.Repo.Repository 1363 } 1364 1365 // Make sure type and URL matches. 1366 if ctx.Params(":type") == "issues" && issue.IsPull { 1367 ctx.Redirect(issue.Link()) 1368 return 1369 } else if ctx.Params(":type") == "pulls" && !issue.IsPull { 1370 ctx.Redirect(issue.Link()) 1371 return 1372 } 1373 1374 if issue.IsPull { 1375 MustAllowPulls(ctx) 1376 if ctx.Written() { 1377 return 1378 } 1379 ctx.Data["PageIsPullList"] = true 1380 ctx.Data["PageIsPullConversation"] = true 1381 } else { 1382 MustEnableIssues(ctx) 1383 if ctx.Written() { 1384 return 1385 } 1386 ctx.Data["PageIsIssueList"] = true 1387 ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) 1388 } 1389 1390 if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) { 1391 ctx.Data["IssueType"] = "pulls" 1392 } else if !issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) { 1393 ctx.Data["IssueType"] = "issues" 1394 } else { 1395 ctx.Data["IssueType"] = "all" 1396 } 1397 1398 ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects) 1399 ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled 1400 upload.AddUploadContext(ctx, "comment") 1401 1402 if err = issue.LoadAttributes(ctx); err != nil { 1403 ctx.ServerError("LoadAttributes", err) 1404 return 1405 } 1406 1407 if err = filterXRefComments(ctx, issue); err != nil { 1408 ctx.ServerError("filterXRefComments", err) 1409 return 1410 } 1411 1412 ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title) 1413 1414 iw := new(issues_model.IssueWatch) 1415 if ctx.Doer != nil { 1416 iw.UserID = ctx.Doer.ID 1417 iw.IssueID = issue.ID 1418 iw.IsWatching, err = issues_model.CheckIssueWatch(ctx, ctx.Doer, issue) 1419 if err != nil { 1420 ctx.ServerError("CheckIssueWatch", err) 1421 return 1422 } 1423 } 1424 ctx.Data["IssueWatch"] = iw 1425 issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ 1426 Links: markup.Links{ 1427 Base: ctx.Repo.RepoLink, 1428 }, 1429 Metas: ctx.Repo.Repository.ComposeMetas(), 1430 GitRepo: ctx.Repo.GitRepo, 1431 Ctx: ctx, 1432 }, issue.Content) 1433 if err != nil { 1434 ctx.ServerError("RenderString", err) 1435 return 1436 } 1437 1438 repo := ctx.Repo.Repository 1439 1440 // Get more information if it's a pull request. 1441 if issue.IsPull { 1442 if issue.PullRequest.HasMerged { 1443 ctx.Data["DisableStatusChange"] = issue.PullRequest.HasMerged 1444 PrepareMergedViewPullInfo(ctx, issue) 1445 } else { 1446 PrepareViewPullInfo(ctx, issue) 1447 ctx.Data["DisableStatusChange"] = ctx.Data["IsPullRequestBroken"] == true && issue.IsClosed 1448 } 1449 if ctx.Written() { 1450 return 1451 } 1452 } 1453 1454 // Metas. 1455 // Check labels. 1456 labelIDMark := make(container.Set[int64]) 1457 for _, label := range issue.Labels { 1458 labelIDMark.Add(label.ID) 1459 } 1460 labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{}) 1461 if err != nil { 1462 ctx.ServerError("GetLabelsByRepoID", err) 1463 return 1464 } 1465 ctx.Data["Labels"] = labels 1466 1467 if repo.Owner.IsOrganization() { 1468 orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) 1469 if err != nil { 1470 ctx.ServerError("GetLabelsByOrgID", err) 1471 return 1472 } 1473 ctx.Data["OrgLabels"] = orgLabels 1474 1475 labels = append(labels, orgLabels...) 1476 } 1477 1478 hasSelected := false 1479 for i := range labels { 1480 if labelIDMark.Contains(labels[i].ID) { 1481 labels[i].IsChecked = true 1482 hasSelected = true 1483 } 1484 } 1485 ctx.Data["HasSelectedLabel"] = hasSelected 1486 1487 // Check milestone and assignee. 1488 if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { 1489 RetrieveRepoMilestonesAndAssignees(ctx, repo) 1490 retrieveProjects(ctx, repo) 1491 1492 if ctx.Written() { 1493 return 1494 } 1495 } 1496 1497 if issue.IsPull { 1498 canChooseReviewer := ctx.Repo.CanWrite(unit.TypePullRequests) 1499 if ctx.Doer != nil && ctx.IsSigned { 1500 if !canChooseReviewer { 1501 canChooseReviewer = ctx.Doer.ID == issue.PosterID 1502 } 1503 if !canChooseReviewer { 1504 canChooseReviewer, err = issues_model.IsOfficialReviewer(ctx, issue, ctx.Doer) 1505 if err != nil { 1506 ctx.ServerError("IsOfficialReviewer", err) 1507 return 1508 } 1509 } 1510 } 1511 1512 RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer) 1513 if ctx.Written() { 1514 return 1515 } 1516 } 1517 1518 if ctx.IsSigned { 1519 // Update issue-user. 1520 if err = activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil { 1521 ctx.ServerError("ReadBy", err) 1522 return 1523 } 1524 } 1525 1526 var ( 1527 role issues_model.RoleDescriptor 1528 ok bool 1529 marked = make(map[int64]issues_model.RoleDescriptor) 1530 comment *issues_model.Comment 1531 participants = make([]*user_model.User, 1, 10) 1532 latestCloseCommentID int64 1533 ) 1534 if ctx.Repo.Repository.IsTimetrackerEnabled(ctx) { 1535 if ctx.IsSigned { 1536 // Deal with the stopwatch 1537 ctx.Data["IsStopwatchRunning"] = issues_model.StopwatchExists(ctx, ctx.Doer.ID, issue.ID) 1538 if !ctx.Data["IsStopwatchRunning"].(bool) { 1539 var exists bool 1540 var swIssue *issues_model.Issue 1541 if exists, _, swIssue, err = issues_model.HasUserStopwatch(ctx, ctx.Doer.ID); err != nil { 1542 ctx.ServerError("HasUserStopwatch", err) 1543 return 1544 } 1545 ctx.Data["HasUserStopwatch"] = exists 1546 if exists { 1547 // Add warning if the user has already a stopwatch 1548 // Add link to the issue of the already running stopwatch 1549 ctx.Data["OtherStopwatchURL"] = swIssue.Link() 1550 } 1551 } 1552 ctx.Data["CanUseTimetracker"] = ctx.Repo.CanUseTimetracker(issue, ctx.Doer) 1553 } else { 1554 ctx.Data["CanUseTimetracker"] = false 1555 } 1556 if ctx.Data["WorkingUsers"], err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil { 1557 ctx.ServerError("TotalTimes", err) 1558 return 1559 } 1560 } 1561 1562 // Check if the user can use the dependencies 1563 ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.Doer, issue.IsPull) 1564 1565 // check if dependencies can be created across repositories 1566 ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies 1567 1568 if issue.ShowRole, err = roleDescriptor(ctx, repo, issue.Poster, issue, issue.HasOriginalAuthor()); err != nil { 1569 ctx.ServerError("roleDescriptor", err) 1570 return 1571 } 1572 marked[issue.PosterID] = issue.ShowRole 1573 1574 // Render comments and and fetch participants. 1575 participants[0] = issue.Poster 1576 for _, comment = range issue.Comments { 1577 comment.Issue = issue 1578 1579 if err := comment.LoadPoster(ctx); err != nil { 1580 ctx.ServerError("LoadPoster", err) 1581 return 1582 } 1583 1584 if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview { 1585 if err := comment.LoadAttachments(ctx); err != nil { 1586 ctx.ServerError("LoadAttachments", err) 1587 return 1588 } 1589 1590 comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ 1591 Links: markup.Links{ 1592 Base: ctx.Repo.RepoLink, 1593 }, 1594 Metas: ctx.Repo.Repository.ComposeMetas(), 1595 GitRepo: ctx.Repo.GitRepo, 1596 Ctx: ctx, 1597 }, comment.Content) 1598 if err != nil { 1599 ctx.ServerError("RenderString", err) 1600 return 1601 } 1602 // Check tag. 1603 role, ok = marked[comment.PosterID] 1604 if ok { 1605 comment.ShowRole = role 1606 continue 1607 } 1608 1609 comment.ShowRole, err = roleDescriptor(ctx, repo, comment.Poster, issue, comment.HasOriginalAuthor()) 1610 if err != nil { 1611 ctx.ServerError("roleDescriptor", err) 1612 return 1613 } 1614 marked[comment.PosterID] = comment.ShowRole 1615 participants = addParticipant(comment.Poster, participants) 1616 } else if comment.Type == issues_model.CommentTypeLabel { 1617 if err = comment.LoadLabel(ctx); err != nil { 1618 ctx.ServerError("LoadLabel", err) 1619 return 1620 } 1621 } else if comment.Type == issues_model.CommentTypeMilestone { 1622 if err = comment.LoadMilestone(ctx); err != nil { 1623 ctx.ServerError("LoadMilestone", err) 1624 return 1625 } 1626 ghostMilestone := &issues_model.Milestone{ 1627 ID: -1, 1628 Name: ctx.Tr("repo.issues.deleted_milestone"), 1629 } 1630 if comment.OldMilestoneID > 0 && comment.OldMilestone == nil { 1631 comment.OldMilestone = ghostMilestone 1632 } 1633 if comment.MilestoneID > 0 && comment.Milestone == nil { 1634 comment.Milestone = ghostMilestone 1635 } 1636 } else if comment.Type == issues_model.CommentTypeProject { 1637 1638 if err = comment.LoadProject(ctx); err != nil { 1639 ctx.ServerError("LoadProject", err) 1640 return 1641 } 1642 1643 ghostProject := &project_model.Project{ 1644 ID: -1, 1645 Title: ctx.Tr("repo.issues.deleted_project"), 1646 } 1647 1648 if comment.OldProjectID > 0 && comment.OldProject == nil { 1649 comment.OldProject = ghostProject 1650 } 1651 1652 if comment.ProjectID > 0 && comment.Project == nil { 1653 comment.Project = ghostProject 1654 } 1655 1656 } else if comment.Type == issues_model.CommentTypeAssignees || comment.Type == issues_model.CommentTypeReviewRequest { 1657 if err = comment.LoadAssigneeUserAndTeam(ctx); err != nil { 1658 ctx.ServerError("LoadAssigneeUserAndTeam", err) 1659 return 1660 } 1661 } else if comment.Type == issues_model.CommentTypeRemoveDependency || comment.Type == issues_model.CommentTypeAddDependency { 1662 if err = comment.LoadDepIssueDetails(ctx); err != nil { 1663 if !issues_model.IsErrIssueNotExist(err) { 1664 ctx.ServerError("LoadDepIssueDetails", err) 1665 return 1666 } 1667 } 1668 } else if comment.Type.HasContentSupport() { 1669 comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ 1670 Links: markup.Links{ 1671 Base: ctx.Repo.RepoLink, 1672 }, 1673 Metas: ctx.Repo.Repository.ComposeMetas(), 1674 GitRepo: ctx.Repo.GitRepo, 1675 Ctx: ctx, 1676 }, comment.Content) 1677 if err != nil { 1678 ctx.ServerError("RenderString", err) 1679 return 1680 } 1681 if err = comment.LoadReview(ctx); err != nil && !issues_model.IsErrReviewNotExist(err) { 1682 ctx.ServerError("LoadReview", err) 1683 return 1684 } 1685 participants = addParticipant(comment.Poster, participants) 1686 if comment.Review == nil { 1687 continue 1688 } 1689 if err = comment.Review.LoadAttributes(ctx); err != nil { 1690 if !user_model.IsErrUserNotExist(err) { 1691 ctx.ServerError("Review.LoadAttributes", err) 1692 return 1693 } 1694 comment.Review.Reviewer = user_model.NewGhostUser() 1695 } 1696 if err = comment.Review.LoadCodeComments(ctx); err != nil { 1697 ctx.ServerError("Review.LoadCodeComments", err) 1698 return 1699 } 1700 for _, codeComments := range comment.Review.CodeComments { 1701 for _, lineComments := range codeComments { 1702 for _, c := range lineComments { 1703 // Check tag. 1704 role, ok = marked[c.PosterID] 1705 if ok { 1706 c.ShowRole = role 1707 continue 1708 } 1709 1710 c.ShowRole, err = roleDescriptor(ctx, repo, c.Poster, issue, c.HasOriginalAuthor()) 1711 if err != nil { 1712 ctx.ServerError("roleDescriptor", err) 1713 return 1714 } 1715 marked[c.PosterID] = c.ShowRole 1716 participants = addParticipant(c.Poster, participants) 1717 } 1718 } 1719 } 1720 if err = comment.LoadResolveDoer(ctx); err != nil { 1721 ctx.ServerError("LoadResolveDoer", err) 1722 return 1723 } 1724 } else if comment.Type == issues_model.CommentTypePullRequestPush { 1725 participants = addParticipant(comment.Poster, participants) 1726 if err = comment.LoadPushCommits(ctx); err != nil { 1727 ctx.ServerError("LoadPushCommits", err) 1728 return 1729 } 1730 } else if comment.Type == issues_model.CommentTypeAddTimeManual || 1731 comment.Type == issues_model.CommentTypeStopTracking || 1732 comment.Type == issues_model.CommentTypeDeleteTimeManual { 1733 // drop error since times could be pruned from DB.. 1734 _ = comment.LoadTime() 1735 if comment.Content != "" { 1736 // Content before v1.21 did store the formated string instead of seconds, 1737 // so "|" is used as delimeter to mark the new format 1738 if comment.Content[0] != '|' { 1739 // handle old time comments that have formatted text stored 1740 comment.RenderedContent = comment.Content 1741 comment.Content = "" 1742 } else { 1743 // else it's just a duration in seconds to pass on to the frontend 1744 comment.Content = comment.Content[1:] 1745 } 1746 } 1747 } 1748 1749 if comment.Type == issues_model.CommentTypeClose || comment.Type == issues_model.CommentTypeMergePull { 1750 // record ID of the latest closed/merged comment. 1751 // if PR is closed, the comments whose type is CommentTypePullRequestPush(29) after latestCloseCommentID won't be rendered. 1752 latestCloseCommentID = comment.ID 1753 } 1754 } 1755 1756 ctx.Data["LatestCloseCommentID"] = latestCloseCommentID 1757 1758 // Combine multiple label assignments into a single comment 1759 combineLabelComments(issue) 1760 1761 getBranchData(ctx, issue) 1762 if issue.IsPull { 1763 pull := issue.PullRequest 1764 pull.Issue = issue 1765 canDelete := false 1766 ctx.Data["AllowMerge"] = false 1767 1768 if ctx.IsSigned { 1769 if err := pull.LoadHeadRepo(ctx); err != nil { 1770 log.Error("LoadHeadRepo: %v", err) 1771 } else if pull.HeadRepo != nil { 1772 perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer) 1773 if err != nil { 1774 ctx.ServerError("GetUserRepoPermission", err) 1775 return 1776 } 1777 if perm.CanWrite(unit.TypeCode) { 1778 // Check if branch is not protected 1779 if pull.HeadBranch != pull.HeadRepo.DefaultBranch { 1780 if protected, err := git_model.IsBranchProtected(ctx, pull.HeadRepo.ID, pull.HeadBranch); err != nil { 1781 log.Error("IsProtectedBranch: %v", err) 1782 } else if !protected { 1783 canDelete = true 1784 ctx.Data["DeleteBranchLink"] = issue.Link() + "/cleanup" 1785 } 1786 } 1787 ctx.Data["CanWriteToHeadRepo"] = true 1788 } 1789 } 1790 1791 if err := pull.LoadBaseRepo(ctx); err != nil { 1792 log.Error("LoadBaseRepo: %v", err) 1793 } 1794 perm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, ctx.Doer) 1795 if err != nil { 1796 ctx.ServerError("GetUserRepoPermission", err) 1797 return 1798 } 1799 ctx.Data["AllowMerge"], err = pull_service.IsUserAllowedToMerge(ctx, pull, perm, ctx.Doer) 1800 if err != nil { 1801 ctx.ServerError("IsUserAllowedToMerge", err) 1802 return 1803 } 1804 1805 if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil { 1806 ctx.ServerError("CanMarkConversation", err) 1807 return 1808 } 1809 } 1810 1811 prUnit, err := repo.GetUnit(ctx, unit.TypePullRequests) 1812 if err != nil { 1813 ctx.ServerError("GetUnit", err) 1814 return 1815 } 1816 prConfig := prUnit.PullRequestsConfig() 1817 1818 var mergeStyle repo_model.MergeStyle 1819 // Check correct values and select default 1820 if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok || 1821 !prConfig.IsMergeStyleAllowed(ms) { 1822 defaultMergeStyle := prConfig.GetDefaultMergeStyle() 1823 if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok { 1824 mergeStyle = defaultMergeStyle 1825 } else if prConfig.AllowMerge { 1826 mergeStyle = repo_model.MergeStyleMerge 1827 } else if prConfig.AllowRebase { 1828 mergeStyle = repo_model.MergeStyleRebase 1829 } else if prConfig.AllowRebaseMerge { 1830 mergeStyle = repo_model.MergeStyleRebaseMerge 1831 } else if prConfig.AllowSquash { 1832 mergeStyle = repo_model.MergeStyleSquash 1833 } else if prConfig.AllowManualMerge { 1834 mergeStyle = repo_model.MergeStyleManuallyMerged 1835 } 1836 } 1837 1838 ctx.Data["MergeStyle"] = mergeStyle 1839 1840 defaultMergeMessage, defaultMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle) 1841 if err != nil { 1842 ctx.ServerError("GetDefaultMergeMessage", err) 1843 return 1844 } 1845 ctx.Data["DefaultMergeMessage"] = defaultMergeMessage 1846 ctx.Data["DefaultMergeBody"] = defaultMergeBody 1847 1848 defaultSquashMergeMessage, defaultSquashMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash) 1849 if err != nil { 1850 ctx.ServerError("GetDefaultSquashMergeMessage", err) 1851 return 1852 } 1853 ctx.Data["DefaultSquashMergeMessage"] = defaultSquashMergeMessage 1854 ctx.Data["DefaultSquashMergeBody"] = defaultSquashMergeBody 1855 1856 pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch) 1857 if err != nil { 1858 ctx.ServerError("LoadProtectedBranch", err) 1859 return 1860 } 1861 ctx.Data["ShowMergeInstructions"] = true 1862 if pb != nil { 1863 pb.Repo = pull.BaseRepo 1864 var showMergeInstructions bool 1865 if ctx.Doer != nil { 1866 showMergeInstructions = pb.CanUserPush(ctx, ctx.Doer) 1867 } 1868 ctx.Data["ProtectedBranch"] = pb 1869 ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pb, pull) 1870 ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull) 1871 ctx.Data["IsBlockedByOfficialReviewRequests"] = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pull) 1872 ctx.Data["IsBlockedByOutdatedBranch"] = issues_model.MergeBlockedByOutdatedBranch(pb, pull) 1873 ctx.Data["GrantedApprovals"] = issues_model.GetGrantedApprovalsCount(ctx, pb, pull) 1874 ctx.Data["RequireSigned"] = pb.RequireSignedCommits 1875 ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles 1876 ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0 1877 ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles) 1878 ctx.Data["ShowMergeInstructions"] = showMergeInstructions 1879 } 1880 ctx.Data["WillSign"] = false 1881 if ctx.Doer != nil { 1882 sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName()) 1883 ctx.Data["WillSign"] = sign 1884 ctx.Data["SigningKey"] = key 1885 if err != nil { 1886 if asymkey_service.IsErrWontSign(err) { 1887 ctx.Data["WontSignReason"] = err.(*asymkey_service.ErrWontSign).Reason 1888 } else { 1889 ctx.Data["WontSignReason"] = "error" 1890 log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err) 1891 } 1892 } 1893 } else { 1894 ctx.Data["WontSignReason"] = "not_signed_in" 1895 } 1896 1897 isPullBranchDeletable := canDelete && 1898 pull.HeadRepo != nil && 1899 git.IsBranchExist(ctx, pull.HeadRepo.RepoPath(), pull.HeadBranch) && 1900 (!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"]) 1901 1902 if isPullBranchDeletable && pull.HasMerged { 1903 exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pull.HeadRepoID, pull.HeadBranch) 1904 if err != nil { 1905 ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err) 1906 return 1907 } 1908 1909 isPullBranchDeletable = !exist 1910 } 1911 ctx.Data["IsPullBranchDeletable"] = isPullBranchDeletable 1912 1913 stillCanManualMerge := func() bool { 1914 if pull.HasMerged || issue.IsClosed || !ctx.IsSigned { 1915 return false 1916 } 1917 if pull.CanAutoMerge() || pull.IsWorkInProgress() || pull.IsChecking() { 1918 return false 1919 } 1920 if (ctx.Doer.IsAdmin || ctx.Repo.IsAdmin()) && prConfig.AllowManualMerge { 1921 return true 1922 } 1923 1924 return false 1925 } 1926 1927 ctx.Data["StillCanManualMerge"] = stillCanManualMerge() 1928 1929 // Check if there is a pending pr merge 1930 ctx.Data["HasPendingPullRequestMerge"], ctx.Data["PendingPullRequestMerge"], err = pull_model.GetScheduledMergeByPullID(ctx, pull.ID) 1931 if err != nil { 1932 ctx.ServerError("GetScheduledMergeByPullID", err) 1933 return 1934 } 1935 } 1936 1937 // Get Dependencies 1938 blockedBy, err := issue.BlockedByDependencies(ctx, db.ListOptions{}) 1939 if err != nil { 1940 ctx.ServerError("BlockedByDependencies", err) 1941 return 1942 } 1943 ctx.Data["BlockedByDependencies"], ctx.Data["BlockedByDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blockedBy) 1944 if ctx.Written() { 1945 return 1946 } 1947 1948 blocking, err := issue.BlockingDependencies(ctx) 1949 if err != nil { 1950 ctx.ServerError("BlockingDependencies", err) 1951 return 1952 } 1953 1954 ctx.Data["BlockingDependencies"], ctx.Data["BlockingDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blocking) 1955 if ctx.Written() { 1956 return 1957 } 1958 1959 var pinAllowed bool 1960 if !issue.IsPinned() { 1961 pinAllowed, err = issues_model.IsNewPinAllowed(ctx, issue.RepoID, issue.IsPull) 1962 if err != nil { 1963 ctx.ServerError("IsNewPinAllowed", err) 1964 return 1965 } 1966 } else { 1967 pinAllowed = true 1968 } 1969 1970 ctx.Data["Participants"] = participants 1971 ctx.Data["NumParticipants"] = len(participants) 1972 ctx.Data["Issue"] = issue 1973 ctx.Data["Reference"] = issue.Ref 1974 ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(ctx.Data["Link"].(string)) 1975 ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID) 1976 ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) 1977 ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(unit.TypeProjects) 1978 ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin) 1979 ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons 1980 ctx.Data["RefEndName"] = git.RefName(issue.Ref).ShortName() 1981 ctx.Data["NewPinAllowed"] = pinAllowed 1982 ctx.Data["PinEnabled"] = setting.Repository.Issue.MaxPinned != 0 1983 1984 var hiddenCommentTypes *big.Int 1985 if ctx.IsSigned { 1986 val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes) 1987 if err != nil { 1988 ctx.ServerError("GetUserSetting", err) 1989 return 1990 } 1991 hiddenCommentTypes, _ = new(big.Int).SetString(val, 10) // we can safely ignore the failed conversion here 1992 } 1993 ctx.Data["ShouldShowCommentType"] = func(commentType issues_model.CommentType) bool { 1994 return hiddenCommentTypes == nil || hiddenCommentTypes.Bit(int(commentType)) == 0 1995 } 1996 // For sidebar 1997 PrepareBranchList(ctx) 1998 1999 if ctx.Written() { 2000 return 2001 } 2002 2003 tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) 2004 if err != nil { 2005 ctx.ServerError("GetTagNamesByRepoID", err) 2006 return 2007 } 2008 ctx.Data["Tags"] = tags 2009 2010 ctx.HTML(http.StatusOK, tplIssueView) 2011 } 2012 2013 // checkBlockedByIssues return canRead and notPermitted 2014 func checkBlockedByIssues(ctx *context.Context, blockers []*issues_model.DependencyInfo) (canRead, notPermitted []*issues_model.DependencyInfo) { 2015 repoPerms := make(map[int64]access_model.Permission) 2016 repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission 2017 for _, blocker := range blockers { 2018 // Get the permissions for this repository 2019 // If the repo ID exists in the map, return the exist permissions 2020 // else get the permission and add it to the map 2021 var perm access_model.Permission 2022 existPerm, ok := repoPerms[blocker.RepoID] 2023 if ok { 2024 perm = existPerm 2025 } else { 2026 var err error 2027 perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer) 2028 if err != nil { 2029 ctx.ServerError("GetUserRepoPermission", err) 2030 return nil, nil 2031 } 2032 repoPerms[blocker.RepoID] = perm 2033 } 2034 if perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) { 2035 canRead = append(canRead, blocker) 2036 } else { 2037 notPermitted = append(notPermitted, blocker) 2038 } 2039 } 2040 sortDependencyInfo(canRead) 2041 sortDependencyInfo(notPermitted) 2042 return canRead, notPermitted 2043 } 2044 2045 func sortDependencyInfo(blockers []*issues_model.DependencyInfo) { 2046 sort.Slice(blockers, func(i, j int) bool { 2047 if blockers[i].RepoID == blockers[j].RepoID { 2048 return blockers[i].Issue.CreatedUnix < blockers[j].Issue.CreatedUnix 2049 } 2050 return blockers[i].RepoID < blockers[j].RepoID 2051 }) 2052 } 2053 2054 // GetActionIssue will return the issue which is used in the context. 2055 func GetActionIssue(ctx *context.Context) *issues_model.Issue { 2056 issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 2057 if err != nil { 2058 ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err) 2059 return nil 2060 } 2061 issue.Repo = ctx.Repo.Repository 2062 checkIssueRights(ctx, issue) 2063 if ctx.Written() { 2064 return nil 2065 } 2066 if err = issue.LoadAttributes(ctx); err != nil { 2067 ctx.ServerError("LoadAttributes", err) 2068 return nil 2069 } 2070 return issue 2071 } 2072 2073 func checkIssueRights(ctx *context.Context, issue *issues_model.Issue) { 2074 if issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) || 2075 !issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) { 2076 ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil) 2077 } 2078 } 2079 2080 func getActionIssues(ctx *context.Context) issues_model.IssueList { 2081 commaSeparatedIssueIDs := ctx.FormString("issue_ids") 2082 if len(commaSeparatedIssueIDs) == 0 { 2083 return nil 2084 } 2085 issueIDs := make([]int64, 0, 10) 2086 for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") { 2087 issueID, err := strconv.ParseInt(stringIssueID, 10, 64) 2088 if err != nil { 2089 ctx.ServerError("ParseInt", err) 2090 return nil 2091 } 2092 issueIDs = append(issueIDs, issueID) 2093 } 2094 issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs) 2095 if err != nil { 2096 ctx.ServerError("GetIssuesByIDs", err) 2097 return nil 2098 } 2099 // Check access rights for all issues 2100 issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues) 2101 prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests) 2102 for _, issue := range issues { 2103 if issue.RepoID != ctx.Repo.Repository.ID { 2104 ctx.NotFound("some issue's RepoID is incorrect", errors.New("some issue's RepoID is incorrect")) 2105 return nil 2106 } 2107 if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled { 2108 ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil) 2109 return nil 2110 } 2111 if err = issue.LoadAttributes(ctx); err != nil { 2112 ctx.ServerError("LoadAttributes", err) 2113 return nil 2114 } 2115 } 2116 return issues 2117 } 2118 2119 // GetIssueInfo get an issue of a repository 2120 func GetIssueInfo(ctx *context.Context) { 2121 issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 2122 if err != nil { 2123 if issues_model.IsErrIssueNotExist(err) { 2124 ctx.Error(http.StatusNotFound) 2125 } else { 2126 ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error()) 2127 } 2128 return 2129 } 2130 2131 if issue.IsPull { 2132 // Need to check if Pulls are enabled and we can read Pulls 2133 if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) { 2134 ctx.Error(http.StatusNotFound) 2135 return 2136 } 2137 } else { 2138 // Need to check if Issues are enabled and we can read Issues 2139 if !ctx.Repo.CanRead(unit.TypeIssues) { 2140 ctx.Error(http.StatusNotFound) 2141 return 2142 } 2143 } 2144 2145 ctx.JSON(http.StatusOK, convert.ToIssue(ctx, issue)) 2146 } 2147 2148 // UpdateIssueTitle change issue's title 2149 func UpdateIssueTitle(ctx *context.Context) { 2150 issue := GetActionIssue(ctx) 2151 if ctx.Written() { 2152 return 2153 } 2154 2155 if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) { 2156 ctx.Error(http.StatusForbidden) 2157 return 2158 } 2159 2160 title := ctx.FormTrim("title") 2161 if len(title) == 0 { 2162 ctx.Error(http.StatusNoContent) 2163 return 2164 } 2165 2166 if err := issue_service.ChangeTitle(ctx, issue, ctx.Doer, title); err != nil { 2167 ctx.ServerError("ChangeTitle", err) 2168 return 2169 } 2170 2171 ctx.JSON(http.StatusOK, map[string]any{ 2172 "title": issue.Title, 2173 }) 2174 } 2175 2176 // UpdateIssueRef change issue's ref (branch) 2177 func UpdateIssueRef(ctx *context.Context) { 2178 issue := GetActionIssue(ctx) 2179 if ctx.Written() { 2180 return 2181 } 2182 2183 if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) || issue.IsPull { 2184 ctx.Error(http.StatusForbidden) 2185 return 2186 } 2187 2188 ref := ctx.FormTrim("ref") 2189 2190 if err := issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, ref); err != nil { 2191 ctx.ServerError("ChangeRef", err) 2192 return 2193 } 2194 2195 ctx.JSON(http.StatusOK, map[string]any{ 2196 "ref": ref, 2197 }) 2198 } 2199 2200 // UpdateIssueContent change issue's content 2201 func UpdateIssueContent(ctx *context.Context) { 2202 issue := GetActionIssue(ctx) 2203 if ctx.Written() { 2204 return 2205 } 2206 2207 if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) { 2208 ctx.Error(http.StatusForbidden) 2209 return 2210 } 2211 2212 if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content")); err != nil { 2213 ctx.ServerError("ChangeContent", err) 2214 return 2215 } 2216 2217 // when update the request doesn't intend to update attachments (eg: change checkbox state), ignore attachment updates 2218 if !ctx.FormBool("ignore_attachments") { 2219 if err := updateAttachments(ctx, issue, ctx.FormStrings("files[]")); err != nil { 2220 ctx.ServerError("UpdateAttachments", err) 2221 return 2222 } 2223 } 2224 2225 content, err := markdown.RenderString(&markup.RenderContext{ 2226 Links: markup.Links{ 2227 Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ? 2228 }, 2229 Metas: ctx.Repo.Repository.ComposeMetas(), 2230 GitRepo: ctx.Repo.GitRepo, 2231 Ctx: ctx, 2232 }, issue.Content) 2233 if err != nil { 2234 ctx.ServerError("RenderString", err) 2235 return 2236 } 2237 2238 ctx.JSON(http.StatusOK, map[string]any{ 2239 "content": content, 2240 "attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content), 2241 }) 2242 } 2243 2244 // UpdateIssueDeadline updates an issue deadline 2245 func UpdateIssueDeadline(ctx *context.Context) { 2246 form := web.GetForm(ctx).(*api.EditDeadlineOption) 2247 issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) 2248 if err != nil { 2249 if issues_model.IsErrIssueNotExist(err) { 2250 ctx.NotFound("GetIssueByIndex", err) 2251 } else { 2252 ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error()) 2253 } 2254 return 2255 } 2256 2257 if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { 2258 ctx.Error(http.StatusForbidden, "", "Not repo writer") 2259 return 2260 } 2261 2262 var deadlineUnix timeutil.TimeStamp 2263 var deadline time.Time 2264 if form.Deadline != nil && !form.Deadline.IsZero() { 2265 deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(), 2266 23, 59, 59, 0, time.Local) 2267 deadlineUnix = timeutil.TimeStamp(deadline.Unix()) 2268 } 2269 2270 if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil { 2271 ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error()) 2272 return 2273 } 2274 2275 ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline}) 2276 } 2277 2278 // UpdateIssueMilestone change issue's milestone 2279 func UpdateIssueMilestone(ctx *context.Context) { 2280 issues := getActionIssues(ctx) 2281 if ctx.Written() { 2282 return 2283 } 2284 2285 milestoneID := ctx.FormInt64("id") 2286 for _, issue := range issues { 2287 oldMilestoneID := issue.MilestoneID 2288 if oldMilestoneID == milestoneID { 2289 continue 2290 } 2291 issue.MilestoneID = milestoneID 2292 if err := issue_service.ChangeMilestoneAssign(issue, ctx.Doer, oldMilestoneID); err != nil { 2293 ctx.ServerError("ChangeMilestoneAssign", err) 2294 return 2295 } 2296 } 2297 2298 ctx.JSONOK() 2299 } 2300 2301 // UpdateIssueAssignee change issue's or pull's assignee 2302 func UpdateIssueAssignee(ctx *context.Context) { 2303 issues := getActionIssues(ctx) 2304 if ctx.Written() { 2305 return 2306 } 2307 2308 assigneeID := ctx.FormInt64("id") 2309 action := ctx.FormString("action") 2310 2311 for _, issue := range issues { 2312 switch action { 2313 case "clear": 2314 if err := issue_service.DeleteNotPassedAssignee(ctx, issue, ctx.Doer, []*user_model.User{}); err != nil { 2315 ctx.ServerError("ClearAssignees", err) 2316 return 2317 } 2318 default: 2319 assignee, err := user_model.GetUserByID(ctx, assigneeID) 2320 if err != nil { 2321 ctx.ServerError("GetUserByID", err) 2322 return 2323 } 2324 2325 valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull) 2326 if err != nil { 2327 ctx.ServerError("canBeAssigned", err) 2328 return 2329 } 2330 if !valid { 2331 ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name}) 2332 return 2333 } 2334 2335 _, _, err = issue_service.ToggleAssigneeWithNotify(ctx, issue, ctx.Doer, assigneeID) 2336 if err != nil { 2337 ctx.ServerError("ToggleAssignee", err) 2338 return 2339 } 2340 } 2341 } 2342 ctx.JSONOK() 2343 } 2344 2345 // UpdatePullReviewRequest add or remove review request 2346 func UpdatePullReviewRequest(ctx *context.Context) { 2347 issues := getActionIssues(ctx) 2348 if ctx.Written() { 2349 return 2350 } 2351 2352 reviewID := ctx.FormInt64("id") 2353 action := ctx.FormString("action") 2354 2355 // TODO: Not support 'clear' now 2356 if action != "attach" && action != "detach" { 2357 ctx.Status(http.StatusForbidden) 2358 return 2359 } 2360 2361 for _, issue := range issues { 2362 if err := issue.LoadRepo(ctx); err != nil { 2363 ctx.ServerError("issue.LoadRepo", err) 2364 return 2365 } 2366 2367 if !issue.IsPull { 2368 log.Warn( 2369 "UpdatePullReviewRequest: refusing to add review request for non-PR issue %-v#%d", 2370 issue.Repo, issue.Index, 2371 ) 2372 ctx.Status(http.StatusForbidden) 2373 return 2374 } 2375 if reviewID < 0 { 2376 // negative reviewIDs represent team requests 2377 if err := issue.Repo.LoadOwner(ctx); err != nil { 2378 ctx.ServerError("issue.Repo.LoadOwner", err) 2379 return 2380 } 2381 2382 if !issue.Repo.Owner.IsOrganization() { 2383 log.Warn( 2384 "UpdatePullReviewRequest: refusing to add team review request for %s#%d owned by non organization UID[%d]", 2385 issue.Repo.FullName(), issue.Index, issue.Repo.ID, 2386 ) 2387 ctx.Status(http.StatusForbidden) 2388 return 2389 } 2390 2391 team, err := organization.GetTeamByID(ctx, -reviewID) 2392 if err != nil { 2393 ctx.ServerError("GetTeamByID", err) 2394 return 2395 } 2396 2397 if team.OrgID != issue.Repo.OwnerID { 2398 log.Warn( 2399 "UpdatePullReviewRequest: refusing to add team review request for UID[%d] team %s to %s#%d owned by UID[%d]", 2400 team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID) 2401 ctx.Status(http.StatusForbidden) 2402 return 2403 } 2404 2405 err = issue_service.IsValidTeamReviewRequest(ctx, team, ctx.Doer, action == "attach", issue) 2406 if err != nil { 2407 if issues_model.IsErrNotValidReviewRequest(err) { 2408 log.Warn( 2409 "UpdatePullReviewRequest: refusing to add invalid team review request for UID[%d] team %s to %s#%d owned by UID[%d]: Error: %v", 2410 team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID, 2411 err, 2412 ) 2413 ctx.Status(http.StatusForbidden) 2414 return 2415 } 2416 ctx.ServerError("IsValidTeamReviewRequest", err) 2417 return 2418 } 2419 2420 _, err = issue_service.TeamReviewRequest(ctx, issue, ctx.Doer, team, action == "attach") 2421 if err != nil { 2422 ctx.ServerError("TeamReviewRequest", err) 2423 return 2424 } 2425 continue 2426 } 2427 2428 reviewer, err := user_model.GetUserByID(ctx, reviewID) 2429 if err != nil { 2430 if user_model.IsErrUserNotExist(err) { 2431 log.Warn( 2432 "UpdatePullReviewRequest: requested reviewer [%d] for %-v to %-v#%d is not exist: Error: %v", 2433 reviewID, issue.Repo, issue.Index, 2434 err, 2435 ) 2436 ctx.Status(http.StatusForbidden) 2437 return 2438 } 2439 ctx.ServerError("GetUserByID", err) 2440 return 2441 } 2442 2443 err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, action == "attach", issue, nil) 2444 if err != nil { 2445 if issues_model.IsErrNotValidReviewRequest(err) { 2446 log.Warn( 2447 "UpdatePullReviewRequest: refusing to add invalid review request for %-v to %-v#%d: Error: %v", 2448 reviewer, issue.Repo, issue.Index, 2449 err, 2450 ) 2451 ctx.Status(http.StatusForbidden) 2452 return 2453 } 2454 ctx.ServerError("isValidReviewRequest", err) 2455 return 2456 } 2457 2458 _, err = issue_service.ReviewRequest(ctx, issue, ctx.Doer, reviewer, action == "attach") 2459 if err != nil { 2460 ctx.ServerError("ReviewRequest", err) 2461 return 2462 } 2463 } 2464 2465 ctx.JSONOK() 2466 } 2467 2468 // SearchIssues searches for issues across the repositories that the user has access to 2469 func SearchIssues(ctx *context.Context) { 2470 before, since, err := context.GetQueryBeforeSince(ctx.Base) 2471 if err != nil { 2472 ctx.Error(http.StatusUnprocessableEntity, err.Error()) 2473 return 2474 } 2475 2476 var isClosed util.OptionalBool 2477 switch ctx.FormString("state") { 2478 case "closed": 2479 isClosed = util.OptionalBoolTrue 2480 case "all": 2481 isClosed = util.OptionalBoolNone 2482 default: 2483 isClosed = util.OptionalBoolFalse 2484 } 2485 2486 var ( 2487 repoIDs []int64 2488 allPublic bool 2489 ) 2490 { 2491 // find repos user can access (for issue search) 2492 opts := &repo_model.SearchRepoOptions{ 2493 Private: false, 2494 AllPublic: true, 2495 TopicOnly: false, 2496 Collaborate: util.OptionalBoolNone, 2497 // This needs to be a column that is not nil in fixtures or 2498 // MySQL will return different results when sorting by null in some cases 2499 OrderBy: db.SearchOrderByAlphabetically, 2500 Actor: ctx.Doer, 2501 } 2502 if ctx.IsSigned { 2503 opts.Private = true 2504 opts.AllLimited = true 2505 } 2506 if ctx.FormString("owner") != "" { 2507 owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner")) 2508 if err != nil { 2509 if user_model.IsErrUserNotExist(err) { 2510 ctx.Error(http.StatusBadRequest, "Owner not found", err.Error()) 2511 } else { 2512 ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error()) 2513 } 2514 return 2515 } 2516 opts.OwnerID = owner.ID 2517 opts.AllLimited = false 2518 opts.AllPublic = false 2519 opts.Collaborate = util.OptionalBoolFalse 2520 } 2521 if ctx.FormString("team") != "" { 2522 if ctx.FormString("owner") == "" { 2523 ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team") 2524 return 2525 } 2526 team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team")) 2527 if err != nil { 2528 if organization.IsErrTeamNotExist(err) { 2529 ctx.Error(http.StatusBadRequest, "Team not found", err.Error()) 2530 } else { 2531 ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error()) 2532 } 2533 return 2534 } 2535 opts.TeamID = team.ID 2536 } 2537 2538 if opts.AllPublic { 2539 allPublic = true 2540 opts.AllPublic = false // set it false to avoid returning too many repos, we could filter by indexer 2541 } 2542 repoIDs, _, err = repo_model.SearchRepositoryIDs(opts) 2543 if err != nil { 2544 ctx.Error(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error()) 2545 return 2546 } 2547 if len(repoIDs) == 0 { 2548 // no repos found, don't let the indexer return all repos 2549 repoIDs = []int64{0} 2550 } 2551 } 2552 2553 keyword := ctx.FormTrim("q") 2554 if strings.IndexByte(keyword, 0) >= 0 { 2555 keyword = "" 2556 } 2557 2558 var isPull util.OptionalBool 2559 switch ctx.FormString("type") { 2560 case "pulls": 2561 isPull = util.OptionalBoolTrue 2562 case "issues": 2563 isPull = util.OptionalBoolFalse 2564 default: 2565 isPull = util.OptionalBoolNone 2566 } 2567 2568 var includedAnyLabels []int64 2569 { 2570 2571 labels := ctx.FormTrim("labels") 2572 var includedLabelNames []string 2573 if len(labels) > 0 { 2574 includedLabelNames = strings.Split(labels, ",") 2575 } 2576 includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames) 2577 if err != nil { 2578 ctx.Error(http.StatusInternalServerError, "GetLabelIDsByNames", err.Error()) 2579 return 2580 } 2581 } 2582 2583 var includedMilestones []int64 2584 { 2585 milestones := ctx.FormTrim("milestones") 2586 var includedMilestoneNames []string 2587 if len(milestones) > 0 { 2588 includedMilestoneNames = strings.Split(milestones, ",") 2589 } 2590 includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames) 2591 if err != nil { 2592 ctx.Error(http.StatusInternalServerError, "GetMilestoneIDsByNames", err.Error()) 2593 return 2594 } 2595 } 2596 2597 var projectID *int64 2598 if v := ctx.FormInt64("project"); v > 0 { 2599 projectID = &v 2600 } 2601 2602 // this api is also used in UI, 2603 // so the default limit is set to fit UI needs 2604 limit := ctx.FormInt("limit") 2605 if limit == 0 { 2606 limit = setting.UI.IssuePagingNum 2607 } else if limit > setting.API.MaxResponseItems { 2608 limit = setting.API.MaxResponseItems 2609 } 2610 2611 searchOpt := &issue_indexer.SearchOptions{ 2612 Paginator: &db.ListOptions{ 2613 Page: ctx.FormInt("page"), 2614 PageSize: limit, 2615 }, 2616 Keyword: keyword, 2617 RepoIDs: repoIDs, 2618 AllPublic: allPublic, 2619 IsPull: isPull, 2620 IsClosed: isClosed, 2621 IncludedAnyLabelIDs: includedAnyLabels, 2622 MilestoneIDs: includedMilestones, 2623 ProjectID: projectID, 2624 SortBy: issue_indexer.SortByCreatedDesc, 2625 } 2626 2627 if since != 0 { 2628 searchOpt.UpdatedAfterUnix = &since 2629 } 2630 if before != 0 { 2631 searchOpt.UpdatedBeforeUnix = &before 2632 } 2633 2634 if ctx.IsSigned { 2635 ctxUserID := ctx.Doer.ID 2636 if ctx.FormBool("created") { 2637 searchOpt.PosterID = &ctxUserID 2638 } 2639 if ctx.FormBool("assigned") { 2640 searchOpt.AssigneeID = &ctxUserID 2641 } 2642 if ctx.FormBool("mentioned") { 2643 searchOpt.MentionID = &ctxUserID 2644 } 2645 if ctx.FormBool("review_requested") { 2646 searchOpt.ReviewRequestedID = &ctxUserID 2647 } 2648 if ctx.FormBool("reviewed") { 2649 searchOpt.ReviewedID = &ctxUserID 2650 } 2651 } 2652 2653 // FIXME: It's unsupported to sort by priority repo when searching by indexer, 2654 // it's indeed an regression, but I think it is worth to support filtering by indexer first. 2655 _ = ctx.FormInt64("priority_repo_id") 2656 2657 ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt) 2658 if err != nil { 2659 ctx.Error(http.StatusInternalServerError, "SearchIssues", err.Error()) 2660 return 2661 } 2662 issues, err := issues_model.GetIssuesByIDs(ctx, ids, true) 2663 if err != nil { 2664 ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err.Error()) 2665 return 2666 } 2667 2668 ctx.SetTotalCountHeader(total) 2669 ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, issues)) 2670 } 2671 2672 func getUserIDForFilter(ctx *context.Context, queryName string) int64 { 2673 userName := ctx.FormString(queryName) 2674 if len(userName) == 0 { 2675 return 0 2676 } 2677 2678 user, err := user_model.GetUserByName(ctx, userName) 2679 if user_model.IsErrUserNotExist(err) { 2680 ctx.NotFound("", err) 2681 return 0 2682 } 2683 2684 if err != nil { 2685 ctx.Error(http.StatusInternalServerError, err.Error()) 2686 return 0 2687 } 2688 2689 return user.ID 2690 } 2691 2692 // ListIssues list the issues of a repository 2693 func ListIssues(ctx *context.Context) { 2694 before, since, err := context.GetQueryBeforeSince(ctx.Base) 2695 if err != nil { 2696 ctx.Error(http.StatusUnprocessableEntity, err.Error()) 2697 return 2698 } 2699 2700 var isClosed util.OptionalBool 2701 switch ctx.FormString("state") { 2702 case "closed": 2703 isClosed = util.OptionalBoolTrue 2704 case "all": 2705 isClosed = util.OptionalBoolNone 2706 default: 2707 isClosed = util.OptionalBoolFalse 2708 } 2709 2710 keyword := ctx.FormTrim("q") 2711 if strings.IndexByte(keyword, 0) >= 0 { 2712 keyword = "" 2713 } 2714 2715 var labelIDs []int64 2716 if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 { 2717 labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, splitted) 2718 if err != nil { 2719 ctx.Error(http.StatusInternalServerError, err.Error()) 2720 return 2721 } 2722 } 2723 2724 var mileIDs []int64 2725 if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 { 2726 for i := range part { 2727 // uses names and fall back to ids 2728 // non existent milestones are discarded 2729 mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx, ctx.Repo.Repository.ID, part[i]) 2730 if err == nil { 2731 mileIDs = append(mileIDs, mile.ID) 2732 continue 2733 } 2734 if !issues_model.IsErrMilestoneNotExist(err) { 2735 ctx.Error(http.StatusInternalServerError, err.Error()) 2736 return 2737 } 2738 id, err := strconv.ParseInt(part[i], 10, 64) 2739 if err != nil { 2740 continue 2741 } 2742 mile, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, id) 2743 if err == nil { 2744 mileIDs = append(mileIDs, mile.ID) 2745 continue 2746 } 2747 if issues_model.IsErrMilestoneNotExist(err) { 2748 continue 2749 } 2750 ctx.Error(http.StatusInternalServerError, err.Error()) 2751 } 2752 } 2753 2754 var projectID *int64 2755 if v := ctx.FormInt64("project"); v > 0 { 2756 projectID = &v 2757 } 2758 2759 var isPull util.OptionalBool 2760 switch ctx.FormString("type") { 2761 case "pulls": 2762 isPull = util.OptionalBoolTrue 2763 case "issues": 2764 isPull = util.OptionalBoolFalse 2765 default: 2766 isPull = util.OptionalBoolNone 2767 } 2768 2769 // FIXME: we should be more efficient here 2770 createdByID := getUserIDForFilter(ctx, "created_by") 2771 if ctx.Written() { 2772 return 2773 } 2774 assignedByID := getUserIDForFilter(ctx, "assigned_by") 2775 if ctx.Written() { 2776 return 2777 } 2778 mentionedByID := getUserIDForFilter(ctx, "mentioned_by") 2779 if ctx.Written() { 2780 return 2781 } 2782 2783 searchOpt := &issue_indexer.SearchOptions{ 2784 Paginator: &db.ListOptions{ 2785 Page: ctx.FormInt("page"), 2786 PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), 2787 }, 2788 Keyword: keyword, 2789 RepoIDs: []int64{ctx.Repo.Repository.ID}, 2790 IsPull: isPull, 2791 IsClosed: isClosed, 2792 ProjectBoardID: projectID, 2793 SortBy: issue_indexer.SortByCreatedDesc, 2794 } 2795 if since != 0 { 2796 searchOpt.UpdatedAfterUnix = &since 2797 } 2798 if before != 0 { 2799 searchOpt.UpdatedBeforeUnix = &before 2800 } 2801 if len(labelIDs) == 1 && labelIDs[0] == 0 { 2802 searchOpt.NoLabelOnly = true 2803 } else { 2804 for _, labelID := range labelIDs { 2805 if labelID > 0 { 2806 searchOpt.IncludedLabelIDs = append(searchOpt.IncludedLabelIDs, labelID) 2807 } else { 2808 searchOpt.ExcludedLabelIDs = append(searchOpt.ExcludedLabelIDs, -labelID) 2809 } 2810 } 2811 } 2812 2813 if len(mileIDs) == 1 && mileIDs[0] == db.NoConditionID { 2814 searchOpt.MilestoneIDs = []int64{0} 2815 } else { 2816 searchOpt.MilestoneIDs = mileIDs 2817 } 2818 2819 if createdByID > 0 { 2820 searchOpt.PosterID = &createdByID 2821 } 2822 if assignedByID > 0 { 2823 searchOpt.AssigneeID = &assignedByID 2824 } 2825 if mentionedByID > 0 { 2826 searchOpt.MentionID = &mentionedByID 2827 } 2828 2829 ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt) 2830 if err != nil { 2831 ctx.Error(http.StatusInternalServerError, "SearchIssues", err.Error()) 2832 return 2833 } 2834 issues, err := issues_model.GetIssuesByIDs(ctx, ids, true) 2835 if err != nil { 2836 ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err.Error()) 2837 return 2838 } 2839 2840 ctx.SetTotalCountHeader(total) 2841 ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, issues)) 2842 } 2843 2844 func BatchDeleteIssues(ctx *context.Context) { 2845 issues := getActionIssues(ctx) 2846 if ctx.Written() { 2847 return 2848 } 2849 for _, issue := range issues { 2850 if err := issue_service.DeleteIssue(ctx, ctx.Doer, ctx.Repo.GitRepo, issue); err != nil { 2851 ctx.ServerError("DeleteIssue", err) 2852 return 2853 } 2854 } 2855 ctx.JSONOK() 2856 } 2857 2858 // UpdateIssueStatus change issue's status 2859 func UpdateIssueStatus(ctx *context.Context) { 2860 issues := getActionIssues(ctx) 2861 if ctx.Written() { 2862 return 2863 } 2864 2865 var isClosed bool 2866 switch action := ctx.FormString("action"); action { 2867 case "open": 2868 isClosed = false 2869 case "close": 2870 isClosed = true 2871 default: 2872 log.Warn("Unrecognized action: %s", action) 2873 } 2874 2875 if _, err := issues.LoadRepositories(ctx); err != nil { 2876 ctx.ServerError("LoadRepositories", err) 2877 return 2878 } 2879 if err := issues.LoadPullRequests(ctx); err != nil { 2880 ctx.ServerError("LoadPullRequests", err) 2881 return 2882 } 2883 2884 for _, issue := range issues { 2885 if issue.IsPull && issue.PullRequest.HasMerged { 2886 continue 2887 } 2888 if issue.IsClosed != isClosed { 2889 if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { 2890 if issues_model.IsErrDependenciesLeft(err) { 2891 ctx.JSON(http.StatusPreconditionFailed, map[string]any{ 2892 "error": ctx.Tr("repo.issues.dependency.issue_batch_close_blocked", issue.Index), 2893 }) 2894 return 2895 } 2896 ctx.ServerError("ChangeStatus", err) 2897 return 2898 } 2899 } 2900 } 2901 ctx.JSONOK() 2902 } 2903 2904 // NewComment create a comment for issue 2905 func NewComment(ctx *context.Context) { 2906 form := web.GetForm(ctx).(*forms.CreateCommentForm) 2907 issue := GetActionIssue(ctx) 2908 if ctx.Written() { 2909 return 2910 } 2911 2912 if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) { 2913 if log.IsTrace() { 2914 if ctx.IsSigned { 2915 issueType := "issues" 2916 if issue.IsPull { 2917 issueType = "pulls" 2918 } 2919 log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+ 2920 "User in Repo has Permissions: %-+v", 2921 ctx.Doer, 2922 issue.PosterID, 2923 issueType, 2924 ctx.Repo.Repository, 2925 ctx.Repo.Permission) 2926 } else { 2927 log.Trace("Permission Denied: Not logged in") 2928 } 2929 } 2930 2931 ctx.Error(http.StatusForbidden) 2932 return 2933 } 2934 2935 if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { 2936 ctx.JSONError(ctx.Tr("repo.issues.comment_on_locked")) 2937 return 2938 } 2939 2940 var attachments []string 2941 if setting.Attachment.Enabled { 2942 attachments = form.Files 2943 } 2944 2945 if ctx.HasError() { 2946 ctx.JSONError(ctx.GetErrMsg()) 2947 return 2948 } 2949 2950 var comment *issues_model.Comment 2951 defer func() { 2952 // Check if issue admin/poster changes the status of issue. 2953 if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.Doer.ID))) && 2954 (form.Status == "reopen" || form.Status == "close") && 2955 !(issue.IsPull && issue.PullRequest.HasMerged) { 2956 2957 // Duplication and conflict check should apply to reopen pull request. 2958 var pr *issues_model.PullRequest 2959 2960 if form.Status == "reopen" && issue.IsPull { 2961 pull := issue.PullRequest 2962 var err error 2963 pr, err = issues_model.GetUnmergedPullRequest(ctx, pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow) 2964 if err != nil { 2965 if !issues_model.IsErrPullRequestNotExist(err) { 2966 ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked")) 2967 return 2968 } 2969 } 2970 2971 // Regenerate patch and test conflict. 2972 if pr == nil { 2973 issue.PullRequest.HeadCommitID = "" 2974 pull_service.AddToTaskQueue(ctx, issue.PullRequest) 2975 } 2976 2977 // check whether the ref of PR <refs/pulls/pr_index/head> in base repo is consistent with the head commit of head branch in the head repo 2978 // get head commit of PR 2979 if pull.Flow == issues_model.PullRequestFlowGithub { 2980 prHeadRef := pull.GetGitRefName() 2981 if err := pull.LoadBaseRepo(ctx); err != nil { 2982 ctx.ServerError("Unable to load base repo", err) 2983 return 2984 } 2985 prHeadCommitID, err := git.GetFullCommitID(ctx, pull.BaseRepo.RepoPath(), prHeadRef) 2986 if err != nil { 2987 ctx.ServerError("Get head commit Id of pr fail", err) 2988 return 2989 } 2990 2991 // get head commit of branch in the head repo 2992 if err := pull.LoadHeadRepo(ctx); err != nil { 2993 ctx.ServerError("Unable to load head repo", err) 2994 return 2995 } 2996 if ok := git.IsBranchExist(ctx, pull.HeadRepo.RepoPath(), pull.BaseBranch); !ok { 2997 // todo localize 2998 ctx.JSONError("The origin branch is delete, cannot reopen.") 2999 return 3000 } 3001 headBranchRef := pull.GetGitHeadBranchRefName() 3002 headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef) 3003 if err != nil { 3004 ctx.ServerError("Get head commit Id of head branch fail", err) 3005 return 3006 } 3007 3008 err = pull.LoadIssue(ctx) 3009 if err != nil { 3010 ctx.ServerError("load the issue of pull request error", err) 3011 return 3012 } 3013 3014 if prHeadCommitID != headBranchCommitID { 3015 // force push to base repo 3016 err := git.Push(ctx, pull.HeadRepo.RepoPath(), git.PushOptions{ 3017 Remote: pull.BaseRepo.RepoPath(), 3018 Branch: pull.HeadBranch + ":" + prHeadRef, 3019 Force: true, 3020 Env: repo_module.InternalPushingEnvironment(pull.Issue.Poster, pull.BaseRepo), 3021 }) 3022 if err != nil { 3023 ctx.ServerError("force push error", err) 3024 return 3025 } 3026 } 3027 } 3028 } 3029 3030 if pr != nil { 3031 ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index)) 3032 } else { 3033 isClosed := form.Status == "close" 3034 if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { 3035 log.Error("ChangeStatus: %v", err) 3036 3037 if issues_model.IsErrDependenciesLeft(err) { 3038 if issue.IsPull { 3039 ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked")) 3040 } else { 3041 ctx.JSONError(ctx.Tr("repo.issues.dependency.issue_close_blocked")) 3042 } 3043 return 3044 } 3045 } else { 3046 if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil { 3047 ctx.ServerError("CreateOrStopIssueStopwatch", err) 3048 return 3049 } 3050 3051 log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed) 3052 } 3053 } 3054 } 3055 3056 // Redirect to comment hashtag if there is any actual content. 3057 typeName := "issues" 3058 if issue.IsPull { 3059 typeName = "pulls" 3060 } 3061 if comment != nil { 3062 ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag())) 3063 } else { 3064 ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index)) 3065 } 3066 }() 3067 3068 // Fix #321: Allow empty comments, as long as we have attachments. 3069 if len(form.Content) == 0 && len(attachments) == 0 { 3070 return 3071 } 3072 3073 comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments) 3074 if err != nil { 3075 ctx.ServerError("CreateIssueComment", err) 3076 return 3077 } 3078 3079 log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID) 3080 } 3081 3082 // UpdateCommentContent change comment of issue's content 3083 func UpdateCommentContent(ctx *context.Context) { 3084 comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) 3085 if err != nil { 3086 ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) 3087 return 3088 } 3089 3090 if err := comment.LoadIssue(ctx); err != nil { 3091 ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err) 3092 return 3093 } 3094 3095 if comment.Issue.RepoID != ctx.Repo.Repository.ID { 3096 ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{}) 3097 return 3098 } 3099 3100 if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { 3101 ctx.Error(http.StatusForbidden) 3102 return 3103 } 3104 3105 if !comment.Type.HasContentSupport() { 3106 ctx.Error(http.StatusNoContent) 3107 return 3108 } 3109 3110 oldContent := comment.Content 3111 comment.Content = ctx.FormString("content") 3112 if len(comment.Content) == 0 { 3113 ctx.JSON(http.StatusOK, map[string]any{ 3114 "content": "", 3115 }) 3116 return 3117 } 3118 if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil { 3119 ctx.ServerError("UpdateComment", err) 3120 return 3121 } 3122 3123 if err := comment.LoadAttachments(ctx); err != nil { 3124 ctx.ServerError("LoadAttachments", err) 3125 return 3126 } 3127 3128 // when the update request doesn't intend to update attachments (eg: change checkbox state), ignore attachment updates 3129 if !ctx.FormBool("ignore_attachments") { 3130 if err := updateAttachments(ctx, comment, ctx.FormStrings("files[]")); err != nil { 3131 ctx.ServerError("UpdateAttachments", err) 3132 return 3133 } 3134 } 3135 3136 content, err := markdown.RenderString(&markup.RenderContext{ 3137 Links: markup.Links{ 3138 Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ? 3139 }, 3140 Metas: ctx.Repo.Repository.ComposeMetas(), 3141 GitRepo: ctx.Repo.GitRepo, 3142 Ctx: ctx, 3143 }, comment.Content) 3144 if err != nil { 3145 ctx.ServerError("RenderString", err) 3146 return 3147 } 3148 3149 ctx.JSON(http.StatusOK, map[string]any{ 3150 "content": content, 3151 "attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content), 3152 }) 3153 } 3154 3155 // DeleteComment delete comment of issue 3156 func DeleteComment(ctx *context.Context) { 3157 comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) 3158 if err != nil { 3159 ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) 3160 return 3161 } 3162 3163 if err := comment.LoadIssue(ctx); err != nil { 3164 ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err) 3165 return 3166 } 3167 3168 if comment.Issue.RepoID != ctx.Repo.Repository.ID { 3169 ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{}) 3170 return 3171 } 3172 3173 if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { 3174 ctx.Error(http.StatusForbidden) 3175 return 3176 } else if !comment.Type.HasContentSupport() { 3177 ctx.Error(http.StatusNoContent) 3178 return 3179 } 3180 3181 if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil { 3182 ctx.ServerError("DeleteComment", err) 3183 return 3184 } 3185 3186 ctx.Status(http.StatusOK) 3187 } 3188 3189 // ChangeIssueReaction create a reaction for issue 3190 func ChangeIssueReaction(ctx *context.Context) { 3191 form := web.GetForm(ctx).(*forms.ReactionForm) 3192 issue := GetActionIssue(ctx) 3193 if ctx.Written() { 3194 return 3195 } 3196 3197 if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) { 3198 if log.IsTrace() { 3199 if ctx.IsSigned { 3200 issueType := "issues" 3201 if issue.IsPull { 3202 issueType = "pulls" 3203 } 3204 log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+ 3205 "User in Repo has Permissions: %-+v", 3206 ctx.Doer, 3207 issue.PosterID, 3208 issueType, 3209 ctx.Repo.Repository, 3210 ctx.Repo.Permission) 3211 } else { 3212 log.Trace("Permission Denied: Not logged in") 3213 } 3214 } 3215 3216 ctx.Error(http.StatusForbidden) 3217 return 3218 } 3219 3220 if ctx.HasError() { 3221 ctx.ServerError("ChangeIssueReaction", errors.New(ctx.GetErrMsg())) 3222 return 3223 } 3224 3225 switch ctx.Params(":action") { 3226 case "react": 3227 reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Content) 3228 if err != nil { 3229 if issues_model.IsErrForbiddenIssueReaction(err) { 3230 ctx.ServerError("ChangeIssueReaction", err) 3231 return 3232 } 3233 log.Info("CreateIssueReaction: %s", err) 3234 break 3235 } 3236 // Reload new reactions 3237 issue.Reactions = nil 3238 if err = issue.LoadAttributes(ctx); err != nil { 3239 log.Info("issue.LoadAttributes: %s", err) 3240 break 3241 } 3242 3243 log.Trace("Reaction for issue created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, reaction.ID) 3244 case "unreact": 3245 if err := issues_model.DeleteIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Content); err != nil { 3246 ctx.ServerError("DeleteIssueReaction", err) 3247 return 3248 } 3249 3250 // Reload new reactions 3251 issue.Reactions = nil 3252 if err := issue.LoadAttributes(ctx); err != nil { 3253 log.Info("issue.LoadAttributes: %s", err) 3254 break 3255 } 3256 3257 log.Trace("Reaction for issue removed: %d/%d", ctx.Repo.Repository.ID, issue.ID) 3258 default: 3259 ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil) 3260 return 3261 } 3262 3263 if len(issue.Reactions) == 0 { 3264 ctx.JSON(http.StatusOK, map[string]any{ 3265 "empty": true, 3266 "html": "", 3267 }) 3268 return 3269 } 3270 3271 html, err := ctx.RenderToString(tplReactions, map[string]any{ 3272 "ctxData": ctx.Data, 3273 "ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index), 3274 "Reactions": issue.Reactions.GroupByType(), 3275 }) 3276 if err != nil { 3277 ctx.ServerError("ChangeIssueReaction.HTMLString", err) 3278 return 3279 } 3280 ctx.JSON(http.StatusOK, map[string]any{ 3281 "html": html, 3282 }) 3283 } 3284 3285 // ChangeCommentReaction create a reaction for comment 3286 func ChangeCommentReaction(ctx *context.Context) { 3287 form := web.GetForm(ctx).(*forms.ReactionForm) 3288 comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) 3289 if err != nil { 3290 ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) 3291 return 3292 } 3293 3294 if err := comment.LoadIssue(ctx); err != nil { 3295 ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err) 3296 return 3297 } 3298 3299 if comment.Issue.RepoID != ctx.Repo.Repository.ID { 3300 ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{}) 3301 return 3302 } 3303 3304 if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) { 3305 if log.IsTrace() { 3306 if ctx.IsSigned { 3307 issueType := "issues" 3308 if comment.Issue.IsPull { 3309 issueType = "pulls" 3310 } 3311 log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+ 3312 "User in Repo has Permissions: %-+v", 3313 ctx.Doer, 3314 comment.Issue.PosterID, 3315 issueType, 3316 ctx.Repo.Repository, 3317 ctx.Repo.Permission) 3318 } else { 3319 log.Trace("Permission Denied: Not logged in") 3320 } 3321 } 3322 3323 ctx.Error(http.StatusForbidden) 3324 return 3325 } 3326 3327 if !comment.Type.HasContentSupport() { 3328 ctx.Error(http.StatusNoContent) 3329 return 3330 } 3331 3332 switch ctx.Params(":action") { 3333 case "react": 3334 reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content) 3335 if err != nil { 3336 if issues_model.IsErrForbiddenIssueReaction(err) { 3337 ctx.ServerError("ChangeIssueReaction", err) 3338 return 3339 } 3340 log.Info("CreateCommentReaction: %s", err) 3341 break 3342 } 3343 // Reload new reactions 3344 comment.Reactions = nil 3345 if err = comment.LoadReactions(ctx, ctx.Repo.Repository); err != nil { 3346 log.Info("comment.LoadReactions: %s", err) 3347 break 3348 } 3349 3350 log.Trace("Reaction for comment created: %d/%d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID, reaction.ID) 3351 case "unreact": 3352 if err := issues_model.DeleteCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content); err != nil { 3353 ctx.ServerError("DeleteCommentReaction", err) 3354 return 3355 } 3356 3357 // Reload new reactions 3358 comment.Reactions = nil 3359 if err = comment.LoadReactions(ctx, ctx.Repo.Repository); err != nil { 3360 log.Info("comment.LoadReactions: %s", err) 3361 break 3362 } 3363 3364 log.Trace("Reaction for comment removed: %d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID) 3365 default: 3366 ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil) 3367 return 3368 } 3369 3370 if len(comment.Reactions) == 0 { 3371 ctx.JSON(http.StatusOK, map[string]any{ 3372 "empty": true, 3373 "html": "", 3374 }) 3375 return 3376 } 3377 3378 html, err := ctx.RenderToString(tplReactions, map[string]any{ 3379 "ctxData": ctx.Data, 3380 "ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID), 3381 "Reactions": comment.Reactions.GroupByType(), 3382 }) 3383 if err != nil { 3384 ctx.ServerError("ChangeCommentReaction.HTMLString", err) 3385 return 3386 } 3387 ctx.JSON(http.StatusOK, map[string]any{ 3388 "html": html, 3389 }) 3390 } 3391 3392 func addParticipant(poster *user_model.User, participants []*user_model.User) []*user_model.User { 3393 for _, part := range participants { 3394 if poster.ID == part.ID { 3395 return participants 3396 } 3397 } 3398 return append(participants, poster) 3399 } 3400 3401 func filterXRefComments(ctx *context.Context, issue *issues_model.Issue) error { 3402 // Remove comments that the user has no permissions to see 3403 for i := 0; i < len(issue.Comments); { 3404 c := issue.Comments[i] 3405 if issues_model.CommentTypeIsRef(c.Type) && c.RefRepoID != issue.RepoID && c.RefRepoID != 0 { 3406 var err error 3407 // Set RefRepo for description in template 3408 c.RefRepo, err = repo_model.GetRepositoryByID(ctx, c.RefRepoID) 3409 if err != nil { 3410 return err 3411 } 3412 perm, err := access_model.GetUserRepoPermission(ctx, c.RefRepo, ctx.Doer) 3413 if err != nil { 3414 return err 3415 } 3416 if !perm.CanReadIssuesOrPulls(c.RefIsPull) { 3417 issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...) 3418 continue 3419 } 3420 } 3421 i++ 3422 } 3423 return nil 3424 } 3425 3426 // GetIssueAttachments returns attachments for the issue 3427 func GetIssueAttachments(ctx *context.Context) { 3428 issue := GetActionIssue(ctx) 3429 if ctx.Written() { 3430 return 3431 } 3432 attachments := make([]*api.Attachment, len(issue.Attachments)) 3433 for i := 0; i < len(issue.Attachments); i++ { 3434 attachments[i] = convert.ToAttachment(ctx.Repo.Repository, issue.Attachments[i]) 3435 } 3436 ctx.JSON(http.StatusOK, attachments) 3437 } 3438 3439 // GetCommentAttachments returns attachments for the comment 3440 func GetCommentAttachments(ctx *context.Context) { 3441 comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) 3442 if err != nil { 3443 ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) 3444 return 3445 } 3446 3447 if err := comment.LoadIssue(ctx); err != nil { 3448 ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err) 3449 return 3450 } 3451 3452 if comment.Issue.RepoID != ctx.Repo.Repository.ID { 3453 ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{}) 3454 return 3455 } 3456 3457 if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) { 3458 ctx.NotFound("CanReadIssuesOrPulls", issues_model.ErrCommentNotExist{}) 3459 return 3460 } 3461 3462 if !comment.Type.HasAttachmentSupport() { 3463 ctx.ServerError("GetCommentAttachments", fmt.Errorf("comment type %v does not support attachments", comment.Type)) 3464 return 3465 } 3466 3467 attachments := make([]*api.Attachment, 0) 3468 if err := comment.LoadAttachments(ctx); err != nil { 3469 ctx.ServerError("LoadAttachments", err) 3470 return 3471 } 3472 for i := 0; i < len(comment.Attachments); i++ { 3473 attachments = append(attachments, convert.ToAttachment(ctx.Repo.Repository, comment.Attachments[i])) 3474 } 3475 ctx.JSON(http.StatusOK, attachments) 3476 } 3477 3478 func updateAttachments(ctx *context.Context, item any, files []string) error { 3479 var attachments []*repo_model.Attachment 3480 switch content := item.(type) { 3481 case *issues_model.Issue: 3482 attachments = content.Attachments 3483 case *issues_model.Comment: 3484 attachments = content.Attachments 3485 default: 3486 return fmt.Errorf("unknown Type: %T", content) 3487 } 3488 for i := 0; i < len(attachments); i++ { 3489 if util.SliceContainsString(files, attachments[i].UUID) { 3490 continue 3491 } 3492 if err := repo_model.DeleteAttachment(ctx, attachments[i], true); err != nil { 3493 return err 3494 } 3495 } 3496 var err error 3497 if len(files) > 0 { 3498 switch content := item.(type) { 3499 case *issues_model.Issue: 3500 err = issues_model.UpdateIssueAttachments(ctx, content.ID, files) 3501 case *issues_model.Comment: 3502 err = content.UpdateAttachments(ctx, files) 3503 default: 3504 return fmt.Errorf("unknown Type: %T", content) 3505 } 3506 if err != nil { 3507 return err 3508 } 3509 } 3510 switch content := item.(type) { 3511 case *issues_model.Issue: 3512 content.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, content.ID) 3513 case *issues_model.Comment: 3514 content.Attachments, err = repo_model.GetAttachmentsByCommentID(ctx, content.ID) 3515 default: 3516 return fmt.Errorf("unknown Type: %T", content) 3517 } 3518 return err 3519 } 3520 3521 func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) string { 3522 attachHTML, err := ctx.RenderToString(tplAttachment, map[string]any{ 3523 "ctxData": ctx.Data, 3524 "Attachments": attachments, 3525 "Content": content, 3526 }) 3527 if err != nil { 3528 ctx.ServerError("attachmentsHTML.HTMLString", err) 3529 return "" 3530 } 3531 return attachHTML 3532 } 3533 3534 // combineLabelComments combine the nearby label comments as one. 3535 func combineLabelComments(issue *issues_model.Issue) { 3536 var prev, cur *issues_model.Comment 3537 for i := 0; i < len(issue.Comments); i++ { 3538 cur = issue.Comments[i] 3539 if i > 0 { 3540 prev = issue.Comments[i-1] 3541 } 3542 if i == 0 || cur.Type != issues_model.CommentTypeLabel || 3543 (prev != nil && prev.PosterID != cur.PosterID) || 3544 (prev != nil && cur.CreatedUnix-prev.CreatedUnix >= 60) { 3545 if cur.Type == issues_model.CommentTypeLabel && cur.Label != nil { 3546 if cur.Content != "1" { 3547 cur.RemovedLabels = append(cur.RemovedLabels, cur.Label) 3548 } else { 3549 cur.AddedLabels = append(cur.AddedLabels, cur.Label) 3550 } 3551 } 3552 continue 3553 } 3554 3555 if cur.Label != nil { // now cur MUST be label comment 3556 if prev.Type == issues_model.CommentTypeLabel { // we can combine them only prev is a label comment 3557 if cur.Content != "1" { 3558 // remove labels from the AddedLabels list if the label that was removed is already 3559 // in this list, and if it's not in this list, add the label to RemovedLabels 3560 addedAndRemoved := false 3561 for i, label := range prev.AddedLabels { 3562 if cur.Label.ID == label.ID { 3563 prev.AddedLabels = append(prev.AddedLabels[:i], prev.AddedLabels[i+1:]...) 3564 addedAndRemoved = true 3565 break 3566 } 3567 } 3568 if !addedAndRemoved { 3569 prev.RemovedLabels = append(prev.RemovedLabels, cur.Label) 3570 } 3571 } else { 3572 // remove labels from the RemovedLabels list if the label that was added is already 3573 // in this list, and if it's not in this list, add the label to AddedLabels 3574 removedAndAdded := false 3575 for i, label := range prev.RemovedLabels { 3576 if cur.Label.ID == label.ID { 3577 prev.RemovedLabels = append(prev.RemovedLabels[:i], prev.RemovedLabels[i+1:]...) 3578 removedAndAdded = true 3579 break 3580 } 3581 } 3582 if !removedAndAdded { 3583 prev.AddedLabels = append(prev.AddedLabels, cur.Label) 3584 } 3585 } 3586 prev.CreatedUnix = cur.CreatedUnix 3587 // remove the current comment since it has been combined to prev comment 3588 issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...) 3589 i-- 3590 } else { // if prev is not a label comment, start a new group 3591 if cur.Content != "1" { 3592 cur.RemovedLabels = append(cur.RemovedLabels, cur.Label) 3593 } else { 3594 cur.AddedLabels = append(cur.AddedLabels, cur.Label) 3595 } 3596 } 3597 } 3598 } 3599 } 3600 3601 // get all teams that current user can mention 3602 func handleTeamMentions(ctx *context.Context) { 3603 if ctx.Doer == nil || !ctx.Repo.Owner.IsOrganization() { 3604 return 3605 } 3606 3607 var isAdmin bool 3608 var err error 3609 var teams []*organization.Team 3610 org := organization.OrgFromUser(ctx.Repo.Owner) 3611 // Admin has super access. 3612 if ctx.Doer.IsAdmin { 3613 isAdmin = true 3614 } else { 3615 isAdmin, err = org.IsOwnedBy(ctx.Doer.ID) 3616 if err != nil { 3617 ctx.ServerError("IsOwnedBy", err) 3618 return 3619 } 3620 } 3621 3622 if isAdmin { 3623 teams, err = org.LoadTeams() 3624 if err != nil { 3625 ctx.ServerError("LoadTeams", err) 3626 return 3627 } 3628 } else { 3629 teams, err = org.GetUserTeams(ctx.Doer.ID) 3630 if err != nil { 3631 ctx.ServerError("GetUserTeams", err) 3632 return 3633 } 3634 } 3635 3636 ctx.Data["MentionableTeams"] = teams 3637 ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name 3638 ctx.Data["MentionableTeamsOrgAvatar"] = ctx.Repo.Owner.AvatarLink(ctx) 3639 } 3640 3641 type userSearchInfo struct { 3642 UserID int64 `json:"user_id"` 3643 UserName string `json:"username"` 3644 AvatarLink string `json:"avatar_link"` 3645 FullName string `json:"full_name"` 3646 } 3647 3648 type userSearchResponse struct { 3649 Results []*userSearchInfo `json:"results"` 3650 } 3651 3652 // IssuePosters get posters for current repo's issues/pull requests 3653 func IssuePosters(ctx *context.Context) { 3654 issuePosters(ctx, false) 3655 } 3656 3657 func PullPosters(ctx *context.Context) { 3658 issuePosters(ctx, true) 3659 } 3660 3661 func issuePosters(ctx *context.Context, isPullList bool) { 3662 repo := ctx.Repo.Repository 3663 search := strings.TrimSpace(ctx.FormString("q")) 3664 posters, err := repo_model.GetIssuePostersWithSearch(ctx, repo, isPullList, search, setting.UI.DefaultShowFullName) 3665 if err != nil { 3666 ctx.JSON(http.StatusInternalServerError, err) 3667 return 3668 } 3669 3670 if search == "" && ctx.Doer != nil { 3671 // the returned posters slice only contains limited number of users, 3672 // to make the current user (doer) can quickly filter their own issues, always add doer to the posters slice 3673 if !slices.ContainsFunc(posters, func(user *user_model.User) bool { return user.ID == ctx.Doer.ID }) { 3674 posters = append(posters, ctx.Doer) 3675 } 3676 } 3677 3678 posters = MakeSelfOnTop(ctx.Doer, posters) 3679 3680 resp := &userSearchResponse{} 3681 resp.Results = make([]*userSearchInfo, len(posters)) 3682 for i, user := range posters { 3683 resp.Results[i] = &userSearchInfo{UserID: user.ID, UserName: user.Name, AvatarLink: user.AvatarLink(ctx)} 3684 if setting.UI.DefaultShowFullName { 3685 resp.Results[i].FullName = user.FullName 3686 } 3687 } 3688 ctx.JSON(http.StatusOK, resp) 3689 }