mirror of https://github.com/go-gitea/gitea.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1009 lines
33 KiB
1009 lines
33 KiB
// 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 |
|
} |
|
}
|
|
|