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