| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010 |
- // Copyright 2024 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package repo
-
- import (
- "fmt"
- "math/big"
- "net/http"
- "net/url"
- "sort"
- "strconv"
-
- activities_model "code.gitea.io/gitea/models/activities"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/organization"
- access_model "code.gitea.io/gitea/models/perm/access"
- project_model "code.gitea.io/gitea/models/project"
- pull_model "code.gitea.io/gitea/models/pull"
- "code.gitea.io/gitea/models/renderhelper"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/emoji"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/markup/markdown"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/templates/vars"
- "code.gitea.io/gitea/modules/util"
- asymkey_service "code.gitea.io/gitea/services/asymkey"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/context/upload"
- issue_service "code.gitea.io/gitea/services/issue"
- pull_service "code.gitea.io/gitea/services/pull"
- user_service "code.gitea.io/gitea/services/user"
- )
-
- // roleDescriptor returns the role descriptor for a comment in/with the given repo, poster and issue
- func roleDescriptor(ctx *context.Context, repo *repo_model.Repository, poster *user_model.User, permsCache map[int64]access_model.Permission, issue *issues_model.Issue, hasOriginalAuthor bool) (roleDesc issues_model.RoleDescriptor, err error) {
- if hasOriginalAuthor {
- // the poster is a migrated user, so no need to detect the role
- return roleDesc, nil
- }
-
- if poster.IsGhost() || !poster.IsIndividual() {
- return roleDesc, nil
- }
-
- roleDesc.IsPoster = issue.IsPoster(poster.ID) // check whether the comment's poster is the issue's poster
-
- // Guess the role of the poster in the repo by permission
- perm, hasPermCache := permsCache[poster.ID]
- if !hasPermCache {
- perm, err = access_model.GetUserRepoPermission(ctx, repo, poster)
- if err != nil {
- return roleDesc, err
- }
- }
- if permsCache != nil {
- permsCache[poster.ID] = perm
- }
-
- // Check if the poster is owner of the repo.
- if perm.IsOwner() {
- // If the poster isn't a site admin, then is must be the repo's owner
- if !poster.IsAdmin {
- roleDesc.RoleInRepo = issues_model.RoleRepoOwner
- return roleDesc, nil
- }
- // Otherwise (poster is site admin), check if poster is the real repo admin.
- isRealRepoAdmin, err := access_model.IsUserRealRepoAdmin(ctx, repo, poster)
- if err != nil {
- return roleDesc, err
- }
- if isRealRepoAdmin {
- roleDesc.RoleInRepo = issues_model.RoleRepoOwner
- return roleDesc, nil
- }
- }
-
- // If repo is organization, check Member role
- if err = repo.LoadOwner(ctx); err != nil {
- return roleDesc, err
- }
- if repo.Owner.IsOrganization() {
- if isMember, err := organization.IsOrganizationMember(ctx, repo.Owner.ID, poster.ID); err != nil {
- return roleDesc, err
- } else if isMember {
- roleDesc.RoleInRepo = issues_model.RoleRepoMember
- return roleDesc, nil
- }
- }
-
- // If the poster is the collaborator of the repo
- if isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, poster.ID); err != nil {
- return roleDesc, err
- } else if isCollaborator {
- roleDesc.RoleInRepo = issues_model.RoleRepoCollaborator
- return roleDesc, nil
- }
-
- hasMergedPR, err := issues_model.HasMergedPullRequestInRepo(ctx, repo.ID, poster.ID)
- if err != nil {
- return roleDesc, err
- } else if hasMergedPR {
- roleDesc.RoleInRepo = issues_model.RoleRepoContributor
- } else if issue.IsPull {
- // only display first time contributor in the first opening pull request
- roleDesc.RoleInRepo = issues_model.RoleRepoFirstTimeContributor
- }
-
- return roleDesc, nil
- }
-
- func getBranchData(ctx *context.Context, issue *issues_model.Issue) {
- ctx.Data["BaseBranch"] = nil
- ctx.Data["HeadBranch"] = nil
- ctx.Data["HeadUserName"] = nil
- ctx.Data["BaseName"] = ctx.Repo.Repository.OwnerName
- if issue.IsPull {
- pull := issue.PullRequest
- ctx.Data["BaseBranch"] = pull.BaseBranch
- ctx.Data["HeadBranch"] = pull.HeadBranch
- ctx.Data["HeadUserName"] = pull.MustHeadUserName(ctx)
- }
- }
-
- // checkBlockedByIssues return canRead and notPermitted
- func checkBlockedByIssues(ctx *context.Context, blockers []*issues_model.DependencyInfo) (canRead, notPermitted []*issues_model.DependencyInfo) {
- repoPerms := make(map[int64]access_model.Permission)
- repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
- for _, blocker := range blockers {
- // Get the permissions for this repository
- // If the repo ID exists in the map, return the exist permissions
- // else get the permission and add it to the map
- var perm access_model.Permission
- existPerm, ok := repoPerms[blocker.RepoID]
- if ok {
- perm = existPerm
- } else {
- var err error
- perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
- if err != nil {
- ctx.ServerError("GetUserRepoPermission", err)
- return nil, nil
- }
- repoPerms[blocker.RepoID] = perm
- }
- if perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) {
- canRead = append(canRead, blocker)
- } else {
- notPermitted = append(notPermitted, blocker)
- }
- }
- sortDependencyInfo(canRead)
- sortDependencyInfo(notPermitted)
- return canRead, notPermitted
- }
-
- func sortDependencyInfo(blockers []*issues_model.DependencyInfo) {
- sort.Slice(blockers, func(i, j int) bool {
- if blockers[i].RepoID == blockers[j].RepoID {
- return blockers[i].Issue.CreatedUnix < blockers[j].Issue.CreatedUnix
- }
- return blockers[i].RepoID < blockers[j].RepoID
- })
- }
-
- func addParticipant(poster *user_model.User, participants []*user_model.User) []*user_model.User {
- for _, part := range participants {
- if poster.ID == part.ID {
- return participants
- }
- }
- return append(participants, poster)
- }
-
- func filterXRefComments(ctx *context.Context, issue *issues_model.Issue) error {
- // Remove comments that the user has no permissions to see
- for i := 0; i < len(issue.Comments); {
- c := issue.Comments[i]
- if issues_model.CommentTypeIsRef(c.Type) && c.RefRepoID != issue.RepoID && c.RefRepoID != 0 {
- var err error
- // Set RefRepo for description in template
- c.RefRepo, err = repo_model.GetRepositoryByID(ctx, c.RefRepoID)
- if err != nil {
- return err
- }
- perm, err := access_model.GetUserRepoPermission(ctx, c.RefRepo, ctx.Doer)
- if err != nil {
- return err
- }
- if !perm.CanReadIssuesOrPulls(c.RefIsPull) {
- issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
- continue
- }
- }
- i++
- }
- return nil
- }
-
- // combineLabelComments combine the nearby label comments as one.
- func combineLabelComments(issue *issues_model.Issue) {
- var prev, cur *issues_model.Comment
- for i := 0; i < len(issue.Comments); i++ {
- cur = issue.Comments[i]
- if i > 0 {
- prev = issue.Comments[i-1]
- }
- if i == 0 || cur.Type != issues_model.CommentTypeLabel ||
- (prev != nil && prev.PosterID != cur.PosterID) ||
- (prev != nil && cur.CreatedUnix-prev.CreatedUnix >= 60) {
- if cur.Type == issues_model.CommentTypeLabel && cur.Label != nil {
- if cur.Content != "1" {
- cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
- } else {
- cur.AddedLabels = append(cur.AddedLabels, cur.Label)
- }
- }
- continue
- }
-
- if cur.Label != nil { // now cur MUST be label comment
- if prev.Type == issues_model.CommentTypeLabel { // we can combine them only prev is a label comment
- if cur.Content != "1" {
- // remove labels from the AddedLabels list if the label that was removed is already
- // in this list, and if it's not in this list, add the label to RemovedLabels
- addedAndRemoved := false
- for i, label := range prev.AddedLabels {
- if cur.Label.ID == label.ID {
- prev.AddedLabels = append(prev.AddedLabels[:i], prev.AddedLabels[i+1:]...)
- addedAndRemoved = true
- break
- }
- }
- if !addedAndRemoved {
- prev.RemovedLabels = append(prev.RemovedLabels, cur.Label)
- }
- } else {
- // remove labels from the RemovedLabels list if the label that was added is already
- // in this list, and if it's not in this list, add the label to AddedLabels
- removedAndAdded := false
- for i, label := range prev.RemovedLabels {
- if cur.Label.ID == label.ID {
- prev.RemovedLabels = append(prev.RemovedLabels[:i], prev.RemovedLabels[i+1:]...)
- removedAndAdded = true
- break
- }
- }
- if !removedAndAdded {
- prev.AddedLabels = append(prev.AddedLabels, cur.Label)
- }
- }
- prev.CreatedUnix = cur.CreatedUnix
- // remove the current comment since it has been combined to prev comment
- issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
- i--
- } else { // if prev is not a label comment, start a new group
- if cur.Content != "1" {
- cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
- } else {
- cur.AddedLabels = append(cur.AddedLabels, cur.Label)
- }
- }
- }
- }
- }
-
- func prepareIssueViewLoad(ctx *context.Context) *issues_model.Issue {
- issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
- if err != nil {
- ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err)
- return nil
- }
- issue.Repo = ctx.Repo.Repository
- ctx.Data["Issue"] = issue
-
- if err = issue.LoadPullRequest(ctx); err != nil {
- ctx.ServerError("LoadPullRequest", err)
- return nil
- }
- return issue
- }
-
- func handleViewIssueRedirectExternal(ctx *context.Context) {
- if ctx.PathParam("type") == "issues" {
- // If issue was requested we check if repo has external tracker and redirect
- extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
- if err == nil && extIssueUnit != nil {
- if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" {
- metas := ctx.Repo.Repository.ComposeCommentMetas(ctx)
- metas["index"] = ctx.PathParam("index")
- res, err := vars.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas)
- if err != nil {
- log.Error("unable to expand template vars for issue url. issue: %s, err: %v", metas["index"], err)
- ctx.ServerError("Expand", err)
- return
- }
- ctx.Redirect(res)
- return
- }
- } else if err != nil && !repo_model.IsErrUnitTypeNotExist(err) {
- ctx.ServerError("GetUnit", err)
- return
- }
- }
- }
-
- // ViewIssue render issue view page
- func ViewIssue(ctx *context.Context) {
- handleViewIssueRedirectExternal(ctx)
- if ctx.Written() {
- return
- }
-
- issue := prepareIssueViewLoad(ctx)
- if ctx.Written() {
- return
- }
-
- // Make sure type and URL matches.
- if ctx.PathParam("type") == "issues" && issue.IsPull {
- ctx.Redirect(issue.Link())
- return
- } else if ctx.PathParam("type") == "pulls" && !issue.IsPull {
- ctx.Redirect(issue.Link())
- return
- }
-
- if issue.IsPull {
- MustAllowPulls(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["PageIsPullList"] = true
- ctx.Data["PageIsPullConversation"] = true
- } else {
- MustEnableIssues(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["PageIsIssueList"] = true
- ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
- }
-
- ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects)
- ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
- upload.AddUploadContext(ctx, "comment")
-
- if err := issue.LoadAttributes(ctx); err != nil {
- ctx.ServerError("LoadAttributes", err)
- return
- }
-
- if err := filterXRefComments(ctx, issue); err != nil {
- ctx.ServerError("filterXRefComments", err)
- return
- }
-
- ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, emoji.ReplaceAliases(issue.Title))
-
- if ctx.IsSigned {
- // Update issue-user.
- if err := activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
- ctx.ServerError("ReadBy", err)
- return
- }
- }
-
- pageMetaData := retrieveRepoIssueMetaData(ctx, ctx.Repo.Repository, issue, issue.IsPull)
- if ctx.Written() {
- return
- }
- pageMetaData.LabelsData.SetSelectedLabels(issue.Labels)
-
- prepareFuncs := []func(*context.Context, *issues_model.Issue){
- prepareIssueViewContent,
- prepareIssueViewCommentsAndSidebarParticipants,
- prepareIssueViewSidebarWatch,
- prepareIssueViewSidebarTimeTracker,
- prepareIssueViewSidebarDependency,
- prepareIssueViewSidebarPin,
- func(ctx *context.Context, issue *issues_model.Issue) { preparePullViewPullInfo(ctx, issue) },
- preparePullViewReviewAndMerge,
- }
-
- for _, prepareFunc := range prepareFuncs {
- prepareFunc(ctx, issue)
- if ctx.Written() {
- return
- }
- }
-
- // Get more information if it's a pull request.
- if issue.IsPull {
- if issue.PullRequest.HasMerged {
- ctx.Data["DisableStatusChange"] = issue.PullRequest.HasMerged
- } else {
- ctx.Data["DisableStatusChange"] = ctx.Data["IsPullRequestBroken"] == true && issue.IsClosed
- }
- }
-
- ctx.Data["Reference"] = issue.Ref
- ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(ctx.Data["Link"].(string))
- ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
- ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
- ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(unit.TypeProjects)
- ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin)
- ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons
- ctx.Data["RefEndName"] = git.RefName(issue.Ref).ShortName()
-
- tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
- if err != nil {
- ctx.ServerError("GetTagNamesByRepoID", err)
- return
- }
- ctx.Data["Tags"] = tags
-
- ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
- return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
- }
-
- if issue.PullRequest != nil && !issue.PullRequest.IsChecking() && !setting.IsProd {
- ctx.Data["PullMergeBoxReloadingInterval"] = 1 // in dev env, force using the reloading logic to make sure it won't break
- }
-
- ctx.HTML(http.StatusOK, tplIssueView)
- }
-
- func ViewPullMergeBox(ctx *context.Context) {
- issue := prepareIssueViewLoad(ctx)
- if !issue.IsPull {
- ctx.NotFound(nil)
- return
- }
- preparePullViewPullInfo(ctx, issue)
- preparePullViewReviewAndMerge(ctx, issue)
- ctx.Data["PullMergeBoxReloading"] = issue.PullRequest.IsChecking()
-
- // TODO: it should use a dedicated struct to render the pull merge box, to make sure all data is prepared correctly
- ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
- ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
- ctx.HTML(http.StatusOK, tplPullMergeBox)
- }
-
- func prepareIssueViewSidebarDependency(ctx *context.Context, issue *issues_model.Issue) {
- if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
- ctx.Data["IssueDependencySearchType"] = "pulls"
- } else if !issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) {
- ctx.Data["IssueDependencySearchType"] = "issues"
- } else {
- ctx.Data["IssueDependencySearchType"] = "all"
- }
-
- // Check if the user can use the dependencies
- ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, issue.IsPull)
-
- // check if dependencies can be created across repositories
- ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies
-
- // Get Dependencies
- blockedBy, err := issue.BlockedByDependencies(ctx, db.ListOptions{})
- if err != nil {
- ctx.ServerError("BlockedByDependencies", err)
- return
- }
- ctx.Data["BlockedByDependencies"], ctx.Data["BlockedByDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blockedBy)
- if ctx.Written() {
- return
- }
-
- blocking, err := issue.BlockingDependencies(ctx)
- if err != nil {
- ctx.ServerError("BlockingDependencies", err)
- return
- }
-
- ctx.Data["BlockingDependencies"], ctx.Data["BlockingDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blocking)
- }
-
- func preparePullViewSigning(ctx *context.Context, issue *issues_model.Issue) {
- if !issue.IsPull {
- return
- }
- pull := issue.PullRequest
- ctx.Data["WillSign"] = false
- if ctx.Doer != nil {
- sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitHeadRefName())
- ctx.Data["WillSign"] = sign
- ctx.Data["SigningKeyMergeDisplay"] = asymkey_model.GetDisplaySigningKey(key)
- if err != nil {
- if asymkey_service.IsErrWontSign(err) {
- ctx.Data["WontSignReason"] = err.(*asymkey_service.ErrWontSign).Reason
- } else {
- ctx.Data["WontSignReason"] = "error"
- log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err)
- }
- }
- } else {
- ctx.Data["WontSignReason"] = "not_signed_in"
- }
- }
-
- func prepareIssueViewSidebarWatch(ctx *context.Context, issue *issues_model.Issue) {
- iw := new(issues_model.IssueWatch)
- if ctx.Doer != nil {
- iw.UserID = ctx.Doer.ID
- iw.IssueID = issue.ID
- var err error
- iw.IsWatching, err = issues_model.CheckIssueWatch(ctx, ctx.Doer, issue)
- if err != nil {
- ctx.ServerError("CheckIssueWatch", err)
- return
- }
- }
- ctx.Data["IssueWatch"] = iw
- }
-
- func prepareIssueViewSidebarTimeTracker(ctx *context.Context, issue *issues_model.Issue) {
- if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
- return
- }
-
- if ctx.IsSigned {
- // Deal with the stopwatch
- ctx.Data["IsStopwatchRunning"] = issues_model.StopwatchExists(ctx, ctx.Doer.ID, issue.ID)
- if !ctx.Data["IsStopwatchRunning"].(bool) {
- exists, _, swIssue, err := issues_model.HasUserStopwatch(ctx, ctx.Doer.ID)
- if err != nil {
- ctx.ServerError("HasUserStopwatch", err)
- return
- }
- ctx.Data["HasUserStopwatch"] = exists
- if exists {
- // Add warning if the user has already a stopwatch
- // Add link to the issue of the already running stopwatch
- ctx.Data["OtherStopwatchURL"] = swIssue.Link()
- }
- }
- ctx.Data["CanUseTimetracker"] = ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer)
- } else {
- ctx.Data["CanUseTimetracker"] = false
- }
- var err error
- if ctx.Data["WorkingUsers"], err = issues_model.TotalTimesForEachUser(ctx, &issues_model.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil {
- ctx.ServerError("TotalTimesForEachUser", err)
- return
- }
- }
-
- func preparePullViewDeleteBranch(ctx *context.Context, issue *issues_model.Issue, canDelete bool) {
- if !issue.IsPull {
- return
- }
- pull := issue.PullRequest
- isPullBranchDeletable := canDelete &&
- pull.HeadRepo != nil &&
- gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.HeadBranch) &&
- (!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"])
-
- if isPullBranchDeletable && pull.HasMerged {
- exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pull.HeadRepoID, pull.HeadBranch)
- if err != nil {
- ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
- return
- }
-
- isPullBranchDeletable = !exist
- }
- ctx.Data["IsPullBranchDeletable"] = isPullBranchDeletable
- }
-
- func prepareIssueViewSidebarPin(ctx *context.Context, issue *issues_model.Issue) {
- var pinAllowed bool
- if err := issue.LoadPinOrder(ctx); err != nil {
- ctx.ServerError("LoadPinOrder", err)
- return
- }
- if issue.PinOrder == 0 {
- var err error
- pinAllowed, err = issues_model.IsNewPinAllowed(ctx, issue.RepoID, issue.IsPull)
- if err != nil {
- ctx.ServerError("IsNewPinAllowed", err)
- return
- }
- } else {
- pinAllowed = true
- }
-
- ctx.Data["NewPinAllowed"] = pinAllowed
- ctx.Data["PinEnabled"] = setting.Repository.Issue.MaxPinned != 0
- }
-
- func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue *issues_model.Issue) {
- var (
- role issues_model.RoleDescriptor
- ok bool
- marked = make(map[int64]issues_model.RoleDescriptor)
- comment *issues_model.Comment
- participants = make([]*user_model.User, 1, 10)
- latestCloseCommentID int64
- err error
- )
-
- marked[issue.PosterID] = issue.ShowRole
-
- // Render comments and fetch participants.
- participants[0] = issue.Poster
-
- if err := issue.Comments.LoadAttachmentsByIssue(ctx); err != nil {
- ctx.ServerError("LoadAttachmentsByIssue", err)
- return
- }
- if err := issue.Comments.LoadPosters(ctx); err != nil {
- ctx.ServerError("LoadPosters", err)
- return
- }
-
- permCache := make(map[int64]access_model.Permission)
-
- for _, comment = range issue.Comments {
- comment.Issue = issue
-
- if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview {
- rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{
- FootnoteContextID: strconv.FormatInt(comment.ID, 10),
- })
- comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content)
- if err != nil {
- ctx.ServerError("RenderString", err)
- return
- }
- // Check tag.
- role, ok = marked[comment.PosterID]
- if ok {
- comment.ShowRole = role
- continue
- }
-
- comment.ShowRole, err = roleDescriptor(ctx, issue.Repo, comment.Poster, permCache, issue, comment.HasOriginalAuthor())
- if err != nil {
- ctx.ServerError("roleDescriptor", err)
- return
- }
- marked[comment.PosterID] = comment.ShowRole
- participants = addParticipant(comment.Poster, participants)
- } else if comment.Type == issues_model.CommentTypeLabel {
- if err = comment.LoadLabel(ctx); err != nil {
- ctx.ServerError("LoadLabel", err)
- return
- }
- } else if comment.Type == issues_model.CommentTypeMilestone {
- if err = comment.LoadMilestone(ctx); err != nil {
- ctx.ServerError("LoadMilestone", err)
- return
- }
- ghostMilestone := &issues_model.Milestone{
- ID: -1,
- Name: ctx.Locale.TrString("repo.issues.deleted_milestone"),
- }
- if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
- comment.OldMilestone = ghostMilestone
- }
- if comment.MilestoneID > 0 && comment.Milestone == nil {
- comment.Milestone = ghostMilestone
- }
- } else if comment.Type == issues_model.CommentTypeProject {
- if err = comment.LoadProject(ctx); err != nil {
- ctx.ServerError("LoadProject", err)
- return
- }
-
- ghostProject := &project_model.Project{
- ID: project_model.GhostProjectID,
- Title: ctx.Locale.TrString("repo.issues.deleted_project"),
- }
-
- if comment.OldProjectID > 0 && comment.OldProject == nil {
- comment.OldProject = ghostProject
- }
-
- if comment.ProjectID > 0 && comment.Project == nil {
- comment.Project = ghostProject
- }
- } else if comment.Type == issues_model.CommentTypeProjectColumn {
- if err = comment.LoadProject(ctx); err != nil {
- ctx.ServerError("LoadProject", err)
- return
- }
- } else if comment.Type == issues_model.CommentTypeAssignees || comment.Type == issues_model.CommentTypeReviewRequest {
- if err = comment.LoadAssigneeUserAndTeam(ctx); err != nil {
- ctx.ServerError("LoadAssigneeUserAndTeam", err)
- return
- }
- } else if comment.Type == issues_model.CommentTypeRemoveDependency || comment.Type == issues_model.CommentTypeAddDependency {
- if err = comment.LoadDepIssueDetails(ctx); err != nil {
- if !issues_model.IsErrIssueNotExist(err) {
- ctx.ServerError("LoadDepIssueDetails", err)
- return
- }
- }
- } else if comment.Type.HasContentSupport() {
- rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{
- FootnoteContextID: strconv.FormatInt(comment.ID, 10),
- })
- comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content)
- if err != nil {
- ctx.ServerError("RenderString", err)
- return
- }
- if err = comment.LoadReview(ctx); err != nil && !issues_model.IsErrReviewNotExist(err) {
- ctx.ServerError("LoadReview", err)
- return
- }
- participants = addParticipant(comment.Poster, participants)
- if comment.Review == nil {
- continue
- }
- if err = comment.Review.LoadAttributes(ctx); err != nil {
- if !user_model.IsErrUserNotExist(err) {
- ctx.ServerError("Review.LoadAttributes", err)
- return
- }
- comment.Review.Reviewer = user_model.NewGhostUser()
- }
- if err = comment.Review.LoadCodeComments(ctx); err != nil {
- ctx.ServerError("Review.LoadCodeComments", err)
- return
- }
- for _, codeComments := range comment.Review.CodeComments {
- for _, lineComments := range codeComments {
- for _, c := range lineComments {
- // Check tag.
- role, ok = marked[c.PosterID]
- if ok {
- c.ShowRole = role
- continue
- }
-
- c.ShowRole, err = roleDescriptor(ctx, issue.Repo, c.Poster, permCache, issue, c.HasOriginalAuthor())
- if err != nil {
- ctx.ServerError("roleDescriptor", err)
- return
- }
- marked[c.PosterID] = c.ShowRole
- participants = addParticipant(c.Poster, participants)
- }
- }
- }
- if err = comment.LoadResolveDoer(ctx); err != nil {
- ctx.ServerError("LoadResolveDoer", err)
- return
- }
- } else if comment.Type == issues_model.CommentTypePullRequestPush {
- participants = addParticipant(comment.Poster, participants)
- if err = issue_service.LoadCommentPushCommits(ctx, comment); err != nil {
- ctx.ServerError("LoadCommentPushCommits", err)
- return
- }
- if !ctx.Repo.CanRead(unit.TypeActions) {
- for _, commit := range comment.Commits {
- if commit.Status == nil {
- continue
- }
- commit.Status.HideActionsURL(ctx)
- git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses)
- }
- }
- } else if comment.Type == issues_model.CommentTypeAddTimeManual ||
- comment.Type == issues_model.CommentTypeStopTracking ||
- comment.Type == issues_model.CommentTypeDeleteTimeManual {
- // drop error since times could be pruned from DB..
- _ = comment.LoadTime(ctx)
- if comment.Content != "" {
- // Content before v1.21 did store the formatted string instead of seconds,
- // so "|" is used as delimiter to mark the new format
- if comment.Content[0] != '|' {
- // handle old time comments that have formatted text stored
- comment.RenderedContent = templates.SanitizeHTML(comment.Content)
- comment.Content = ""
- } else {
- // else it's just a duration in seconds to pass on to the frontend
- comment.Content = comment.Content[1:]
- }
- }
- }
-
- if comment.Type == issues_model.CommentTypeClose || comment.Type == issues_model.CommentTypeMergePull {
- // record ID of the latest closed/merged comment.
- // if PR is closed, the comments whose type is CommentTypePullRequestPush(29) after latestCloseCommentID won't be rendered.
- latestCloseCommentID = comment.ID
- }
- }
-
- ctx.Data["LatestCloseCommentID"] = latestCloseCommentID
-
- // Combine multiple label assignments into a single comment
- combineLabelComments(issue)
-
- var hiddenCommentTypes *big.Int
- if ctx.IsSigned {
- val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes)
- if err != nil {
- ctx.ServerError("GetUserSetting", err)
- return
- }
- hiddenCommentTypes, _ = new(big.Int).SetString(val, 10) // we can safely ignore the failed conversion here
- }
- ctx.Data["ShouldShowCommentType"] = func(commentType issues_model.CommentType) bool {
- return hiddenCommentTypes == nil || hiddenCommentTypes.Bit(int(commentType)) == 0
- }
-
- // prepare for sidebar participants
- ctx.Data["Participants"] = participants
- ctx.Data["NumParticipants"] = len(participants)
- }
-
- func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Issue) {
- getBranchData(ctx, issue)
- if !issue.IsPull {
- return
- }
-
- pull := issue.PullRequest
- pull.Issue = issue
- canDelete := false
- allowMerge := false
- canWriteToHeadRepo := false
-
- pull_service.StartPullRequestCheckOnView(ctx, pull)
-
- if ctx.IsSigned {
- if err := pull.LoadHeadRepo(ctx); err != nil {
- log.Error("LoadHeadRepo: %v", err)
- } else if pull.HeadRepo != nil {
- perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
- if err != nil {
- ctx.ServerError("GetUserRepoPermission", err)
- return
- }
- if perm.CanWrite(unit.TypeCode) {
- // Check if branch is not protected
- if pull.HeadBranch != pull.HeadRepo.DefaultBranch {
- if protected, err := git_model.IsBranchProtected(ctx, pull.HeadRepo.ID, pull.HeadBranch); err != nil {
- log.Error("IsProtectedBranch: %v", err)
- } else if !protected {
- canDelete = true
- ctx.Data["DeleteBranchLink"] = issue.Link() + "/cleanup"
- }
- }
- canWriteToHeadRepo = true
- }
- }
-
- if err := pull.LoadBaseRepo(ctx); err != nil {
- log.Error("LoadBaseRepo: %v", err)
- }
- perm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, ctx.Doer)
- if err != nil {
- ctx.ServerError("GetUserRepoPermission", err)
- return
- }
- if !canWriteToHeadRepo { // maintainers maybe allowed to push to head repo even if they can't write to it
- canWriteToHeadRepo = pull.AllowMaintainerEdit && perm.CanWrite(unit.TypeCode)
- }
- allowMerge, err = pull_service.IsUserAllowedToMerge(ctx, pull, perm, ctx.Doer)
- if err != nil {
- ctx.ServerError("IsUserAllowedToMerge", err)
- return
- }
-
- if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil {
- ctx.ServerError("CanMarkConversation", err)
- return
- }
- }
-
- ctx.Data["PullMergeBoxReloadingInterval"] = util.Iif(pull != nil && pull.IsChecking(), 2000, 0)
- ctx.Data["CanWriteToHeadRepo"] = canWriteToHeadRepo
- ctx.Data["ShowMergeInstructions"] = canWriteToHeadRepo
- ctx.Data["AllowMerge"] = allowMerge
-
- prUnit, err := issue.Repo.GetUnit(ctx, unit.TypePullRequests)
- if err != nil {
- ctx.ServerError("GetUnit", err)
- return
- }
- prConfig := prUnit.PullRequestsConfig()
-
- ctx.Data["AutodetectManualMerge"] = prConfig.AutodetectManualMerge
-
- var mergeStyle repo_model.MergeStyle
- // Check correct values and select default
- if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok ||
- !prConfig.IsMergeStyleAllowed(ms) {
- defaultMergeStyle := prConfig.GetDefaultMergeStyle()
- if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok {
- mergeStyle = defaultMergeStyle
- } else if prConfig.AllowMerge {
- mergeStyle = repo_model.MergeStyleMerge
- } else if prConfig.AllowRebase {
- mergeStyle = repo_model.MergeStyleRebase
- } else if prConfig.AllowRebaseMerge {
- mergeStyle = repo_model.MergeStyleRebaseMerge
- } else if prConfig.AllowSquash {
- mergeStyle = repo_model.MergeStyleSquash
- } else if prConfig.AllowFastForwardOnly {
- mergeStyle = repo_model.MergeStyleFastForwardOnly
- } else if prConfig.AllowManualMerge {
- mergeStyle = repo_model.MergeStyleManuallyMerged
- }
- }
-
- ctx.Data["MergeStyle"] = mergeStyle
-
- defaultMergeMessage, defaultMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle)
- if err != nil {
- ctx.ServerError("GetDefaultMergeMessage", err)
- return
- }
- ctx.Data["DefaultMergeMessage"] = defaultMergeMessage
- ctx.Data["DefaultMergeBody"] = defaultMergeBody
-
- defaultSquashMergeMessage, defaultSquashMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash)
- if err != nil {
- ctx.ServerError("GetDefaultSquashMergeMessage", err)
- return
- }
- ctx.Data["DefaultSquashMergeMessage"] = defaultSquashMergeMessage
- ctx.Data["DefaultSquashMergeBody"] = defaultSquashMergeBody
-
- pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
- if err != nil {
- ctx.ServerError("LoadProtectedBranch", err)
- return
- }
-
- if pb != nil {
- pb.Repo = pull.BaseRepo
- ctx.Data["ProtectedBranch"] = pb
- ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pb, pull)
- ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull)
- ctx.Data["IsBlockedByOfficialReviewRequests"] = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pull)
- ctx.Data["IsBlockedByOutdatedBranch"] = issues_model.MergeBlockedByOutdatedBranch(pb, pull)
- ctx.Data["GrantedApprovals"] = issues_model.GetGrantedApprovalsCount(ctx, pb, pull)
- ctx.Data["RequireSigned"] = pb.RequireSignedCommits
- ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles
- ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0
- ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles)
- ctx.Data["RequireApprovalsWhitelist"] = pb.EnableApprovalsWhitelist
- }
-
- preparePullViewSigning(ctx, issue)
- if ctx.Written() {
- return
- }
-
- preparePullViewDeleteBranch(ctx, issue, canDelete)
- if ctx.Written() {
- return
- }
-
- stillCanManualMerge := func() bool {
- if pull.HasMerged || issue.IsClosed || !ctx.IsSigned {
- return false
- }
- if pull.CanAutoMerge() || pull.IsWorkInProgress(ctx) || pull.IsChecking() {
- return false
- }
- if allowMerge && prConfig.AllowManualMerge {
- return true
- }
-
- return false
- }
-
- ctx.Data["StillCanManualMerge"] = stillCanManualMerge()
-
- // Check if there is a pending pr merge
- ctx.Data["HasPendingPullRequestMerge"], ctx.Data["PendingPullRequestMerge"], err = pull_model.GetScheduledMergeByPullID(ctx, pull.ID)
- if err != nil {
- ctx.ServerError("GetScheduledMergeByPullID", err)
- return
- }
- }
-
- func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) {
- var err error
- rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{
- FootnoteContextID: "0", // Set footnote context ID to 0 for the issue content
- })
- issue.RenderedContent, err = markdown.RenderString(rctx, issue.Content)
- if err != nil {
- ctx.ServerError("RenderString", err)
- return
- }
- if issue.ShowRole, err = roleDescriptor(ctx, issue.Repo, issue.Poster, nil, issue, issue.HasOriginalAuthor()); err != nil {
- ctx.ServerError("roleDescriptor", err)
- return
- }
- }
|