2014-03-22 21:50:50 +04:00
// Copyright 2014 The Gogs Authors. All rights reserved.
2018-11-28 14:26:14 +03:00
// Copyright 2018 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2014-03-22 21:50:50 +04:00
package repo
2014-07-26 10:28:04 +04:00
import (
2017-01-25 05:43:02 +03:00
"bytes"
2022-04-28 14:48:48 +03:00
stdCtx "context"
2014-07-26 10:28:04 +04:00
"errors"
"fmt"
2022-01-21 20:59:26 +03:00
"math/big"
2018-07-18 00:23:58 +03:00
"net/http"
2021-11-16 21:18:25 +03:00
"net/url"
2022-09-02 10:58:49 +03:00
"sort"
2017-03-15 04:10:35 +03:00
"strconv"
2014-07-26 10:28:04 +04:00
"strings"
2022-04-07 21:59:56 +03:00
"time"
2014-07-26 10:28:04 +04:00
2022-08-25 05:31:57 +03:00
activities_model "code.gitea.io/gitea/models/activities"
2021-09-24 14:32:56 +03:00
"code.gitea.io/gitea/models/db"
2022-06-12 18:51:54 +03:00
git_model "code.gitea.io/gitea/models/git"
2022-03-31 12:20:39 +03:00
issues_model "code.gitea.io/gitea/models/issues"
2022-03-29 09:29:02 +03:00
"code.gitea.io/gitea/models/organization"
2022-05-11 13:09:36 +03:00
access_model "code.gitea.io/gitea/models/perm/access"
2022-03-29 17:16:31 +03:00
project_model "code.gitea.io/gitea/models/project"
2022-05-07 20:05:52 +03:00
pull_model "code.gitea.io/gitea/models/pull"
2021-11-19 16:39:57 +03:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-09 22:57:58 +03:00
"code.gitea.io/gitea/models/unit"
2021-11-24 12:49:20 +03:00
user_model "code.gitea.io/gitea/models/user"
2016-11-10 19:24:48 +03:00
"code.gitea.io/gitea/modules/base"
2022-10-12 08:18:26 +03:00
"code.gitea.io/gitea/modules/container"
2016-11-10 19:24:48 +03:00
"code.gitea.io/gitea/modules/context"
2019-03-27 12:33:00 +03:00
"code.gitea.io/gitea/modules/git"
2019-02-21 03:54:05 +03:00
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
2022-09-02 10:58:49 +03:00
issue_template "code.gitea.io/gitea/modules/issue/template"
2016-11-10 19:24:48 +03:00
"code.gitea.io/gitea/modules/log"
2019-12-07 07:21:18 +03:00
"code.gitea.io/gitea/modules/markup"
2017-09-21 08:20:14 +03:00
"code.gitea.io/gitea/modules/markup/markdown"
2016-11-10 19:24:48 +03:00
"code.gitea.io/gitea/modules/setting"
2019-06-06 03:37:45 +03:00
api "code.gitea.io/gitea/modules/structs"
2022-04-01 11:47:50 +03:00
"code.gitea.io/gitea/modules/templates/vars"
2022-04-07 21:59:56 +03:00
"code.gitea.io/gitea/modules/timeutil"
2020-10-05 08:49:33 +03:00
"code.gitea.io/gitea/modules/upload"
2017-01-25 05:43:02 +03:00
"code.gitea.io/gitea/modules/util"
2021-01-26 18:36:53 +03:00
"code.gitea.io/gitea/modules/web"
2022-09-02 10:58:49 +03:00
"code.gitea.io/gitea/routers/utils"
2021-12-10 11:14:24 +03:00
asymkey_service "code.gitea.io/gitea/services/asymkey"
2022-12-29 05:57:15 +03:00
"code.gitea.io/gitea/services/convert"
2021-04-06 22:44:05 +03:00
"code.gitea.io/gitea/services/forms"
2019-09-30 16:50:44 +03:00
issue_service "code.gitea.io/gitea/services/issue"
2019-12-07 05:44:10 +03:00
pull_service "code.gitea.io/gitea/services/pull"
2022-06-06 11:01:49 +03:00
repo_service "code.gitea.io/gitea/services/repository"
2014-07-26 10:28:04 +04:00
)
const (
2019-10-15 15:19:32 +03:00
tplAttachment base . TplName = "repo/issue/view_content/attachments"
2020-09-11 17:48:39 +03:00
tplIssues base . TplName = "repo/issue/list"
tplIssueNew base . TplName = "repo/issue/new"
tplIssueChoose base . TplName = "repo/issue/choose"
tplIssueView base . TplName = "repo/issue/view"
2014-07-26 10:28:04 +04:00
2017-12-04 02:14:26 +03:00
tplReactions base . TplName = "repo/issue/view_content/reactions"
2020-09-11 17:48:39 +03:00
issueTemplateKey = "IssueTemplate"
issueTemplateTitleKey = "IssueTemplateTitle"
2014-07-26 10:28:04 +04:00
)
2022-01-20 20:46:10 +03:00
// IssueTemplateCandidates issue templates
var IssueTemplateCandidates = [ ] string {
"ISSUE_TEMPLATE.md" ,
2022-09-02 10:58:49 +03:00
"ISSUE_TEMPLATE.yaml" ,
"ISSUE_TEMPLATE.yml" ,
2022-01-20 20:46:10 +03:00
"issue_template.md" ,
2022-09-02 10:58:49 +03:00
"issue_template.yaml" ,
"issue_template.yml" ,
2022-01-20 20:46:10 +03:00
".gitea/ISSUE_TEMPLATE.md" ,
2022-09-02 10:58:49 +03:00
".gitea/ISSUE_TEMPLATE.yaml" ,
".gitea/ISSUE_TEMPLATE.yml" ,
".gitea/issue_template.md" ,
".gitea/issue_template.yaml" ,
2022-09-09 06:22:33 +03:00
".gitea/issue_template.yml" ,
2022-01-20 20:46:10 +03:00
".github/ISSUE_TEMPLATE.md" ,
2022-09-02 10:58:49 +03:00
".github/ISSUE_TEMPLATE.yaml" ,
".github/ISSUE_TEMPLATE.yml" ,
2022-01-20 20:46:10 +03:00
".github/issue_template.md" ,
2022-09-02 10:58:49 +03:00
".github/issue_template.yaml" ,
".github/issue_template.yml" ,
2022-01-20 20:46:10 +03:00
}
2014-07-26 10:28:04 +04:00
2019-02-18 23:55:04 +03:00
// MustAllowUserComment checks to make sure if an issue is locked.
// If locked and user has permissions to write to the repository,
// then the comment is allowed, else it is blocked
func MustAllowUserComment ( ctx * context . Context ) {
issue := GetActionIssue ( ctx )
if ctx . Written ( ) {
return
}
2022-03-22 10:03:22 +03:00
if issue . IsLocked && ! ctx . Repo . CanWriteIssuesOrPulls ( issue . IsPull ) && ! ctx . Doer . IsAdmin {
2019-02-18 23:55:04 +03:00
ctx . Flash . Error ( ctx . Tr ( "repo.issues.comment_on_locked" ) )
2023-02-11 09:34:11 +03:00
ctx . Redirect ( issue . Link ( ) )
2019-02-18 23:55:04 +03:00
return
}
}
2016-11-24 10:04:31 +03:00
// MustEnableIssues check if repository enable internal issues
2016-03-11 19:56:52 +03:00
func MustEnableIssues ( ctx * context . Context ) {
2021-11-09 22:57:58 +03:00
if ! ctx . Repo . CanRead ( unit . TypeIssues ) &&
! ctx . Repo . CanRead ( unit . TypeExternalTracker ) {
2018-01-11 00:34:17 +03:00
ctx . NotFound ( "MustEnableIssues" , nil )
2016-03-07 07:57:46 +03:00
return
2015-12-05 05:30:33 +03:00
}
2016-11-04 11:06:54 +03:00
2022-12-10 05:46:31 +03:00
unit , err := ctx . Repo . Repository . GetUnit ( ctx , unit . TypeExternalTracker )
2017-02-04 18:53:46 +03:00
if err == nil {
ctx . Redirect ( unit . ExternalTrackerConfig ( ) . ExternalTrackerURL )
2016-11-04 11:06:54 +03:00
return
}
2015-12-05 05:30:33 +03:00
}
2018-11-28 14:26:14 +03:00
// MustAllowPulls check if repository enable pull requests and user have right to do that
2016-03-11 19:56:52 +03:00
func MustAllowPulls ( ctx * context . Context ) {
2021-11-09 22:57:58 +03:00
if ! ctx . Repo . Repository . CanEnablePulls ( ) || ! ctx . Repo . CanRead ( unit . TypePullRequests ) {
2018-01-11 00:34:17 +03:00
ctx . NotFound ( "MustAllowPulls" , nil )
2016-03-07 07:57:46 +03:00
return
2015-12-05 05:30:33 +03:00
}
2015-12-20 06:07:06 +03:00
2016-03-07 07:57:46 +03:00
// User can send pull request if owns a forked repository.
2022-03-22 10:03:22 +03:00
if ctx . IsSigned && repo_model . HasForkedRepo ( ctx . Doer . ID , ctx . Repo . Repository . ID ) {
2016-03-07 07:57:46 +03:00
ctx . Repo . PullRequest . Allowed = true
2022-03-22 10:03:22 +03:00
ctx . Repo . PullRequest . HeadInfoSubURL = url . PathEscape ( ctx . Doer . Name ) + ":" + util . PathEscapeSegments ( ctx . Repo . BranchName )
2016-03-07 07:57:46 +03:00
}
2015-12-05 05:30:33 +03:00
}
2020-08-17 06:07:38 +03:00
func issues ( ctx * context . Context , milestoneID , projectID int64 , isPullOption util . OptionalBool ) {
2018-11-29 04:46:30 +03:00
var err error
2021-08-11 03:31:13 +03:00
viewType := ctx . FormString ( "type" )
sortType := ctx . FormString ( "sort" )
2023-02-25 05:55:50 +03:00
types := [ ] string { "all" , "your_repositories" , "assigned" , "created_by" , "mentioned" , "review_requested" , "reviewed_by" }
Improve utils of slices (#22379)
- Move the file `compare.go` and `slice.go` to `slice.go`.
- Fix `ExistsInSlice`, it's buggy
- It uses `sort.Search`, so it assumes that the input slice is sorted.
- It passes `func(i int) bool { return slice[i] == target })` to
`sort.Search`, that's incorrect, check the doc of `sort.Search`.
- Conbine `IsInt64InSlice(int64, []int64)` and `ExistsInSlice(string,
[]string)` to `SliceContains[T]([]T, T)`.
- Conbine `IsSliceInt64Eq([]int64, []int64)` and `IsEqualSlice([]string,
[]string)` to `SliceSortedEqual[T]([]T, T)`.
- Add `SliceEqual[T]([]T, T)` as a distinction from
`SliceSortedEqual[T]([]T, T)`.
- Redesign `RemoveIDFromList([]int64, int64) ([]int64, bool)` to
`SliceRemoveAll[T]([]T, T) []T`.
- Add `SliceContainsFunc[T]([]T, func(T) bool)` and
`SliceRemoveAllFunc[T]([]T, func(T) bool)` for general use.
- Add comments to explain why not `golang.org/x/exp/slices`.
- Add unit tests.
2023-01-11 08:31:16 +03:00
if ! util . SliceContainsString ( types , viewType , true ) {
2014-07-26 10:28:04 +04:00
viewType = "all"
}
2015-08-15 07:07:08 +03:00
var (
2021-07-29 04:42:15 +03:00
assigneeID = ctx . FormInt64 ( "assignee" )
2022-08-08 23:03:58 +03:00
posterID = ctx . FormInt64 ( "poster" )
2021-01-17 19:34:19 +03:00
mentionedID int64
reviewRequestedID int64
2023-02-25 05:55:50 +03:00
reviewedID int64
2021-01-17 19:34:19 +03:00
forceEmpty bool
2015-08-15 07:07:08 +03:00
)
2014-07-26 10:28:04 +04:00
2017-06-15 06:09:03 +03:00
if ctx . IsSigned {
switch viewType {
case "created_by" :
2022-03-22 10:03:22 +03:00
posterID = ctx . Doer . ID
2017-06-15 06:09:03 +03:00
case "mentioned" :
2022-03-22 10:03:22 +03:00
mentionedID = ctx . Doer . ID
2020-11-20 00:39:55 +03:00
case "assigned" :
2022-03-22 10:03:22 +03:00
assigneeID = ctx . Doer . ID
2021-01-17 19:34:19 +03:00
case "review_requested" :
2022-03-22 10:03:22 +03:00
reviewRequestedID = ctx . Doer . ID
2023-02-25 05:55:50 +03:00
case "reviewed_by" :
reviewedID = ctx . Doer . ID
2017-06-15 06:09:03 +03:00
}
}
2015-07-24 21:52:25 +03:00
repo := ctx . Repo . Repository
2019-01-23 07:10:38 +03:00
var labelIDs [ ] int64
2021-08-11 03:31:13 +03:00
selectLabels := ctx . FormString ( "labels" )
2019-01-23 07:10:38 +03:00
if len ( selectLabels ) > 0 && selectLabels != "0" {
labelIDs , err = base . StringsToInt64s ( strings . Split ( selectLabels , "," ) )
if err != nil {
ctx . ServerError ( "StringsToInt64s" , err )
return
}
}
2016-12-24 13:33:21 +03:00
2021-08-11 03:31:13 +03:00
keyword := strings . Trim ( ctx . FormString ( "q" ) , " " )
2017-01-25 05:43:02 +03:00
if bytes . Contains ( [ ] byte ( keyword ) , [ ] byte { 0x00 } ) {
keyword = ""
}
var issueIDs [ ] int64
if len ( keyword ) > 0 {
2022-01-27 11:30:51 +03:00
issueIDs , err = issue_indexer . SearchIssuesByKeyword ( ctx , [ ] int64 { repo . ID } , keyword )
2019-02-19 17:39:39 +03:00
if err != nil {
2022-01-27 11:30:51 +03:00
if issue_indexer . IsAvailable ( ) {
ctx . ServerError ( "issueIndexer.Search" , err )
return
}
ctx . Data [ "IssueIndexerUnavailable" ] = true
2019-02-19 17:39:39 +03:00
}
2017-01-25 05:43:02 +03:00
if len ( issueIDs ) == 0 {
forceEmpty = true
}
}
2022-06-13 12:37:59 +03:00
var issueStats * issues_model . IssueStats
2016-12-24 13:33:21 +03:00
if forceEmpty {
2022-06-13 12:37:59 +03:00
issueStats = & issues_model . IssueStats { }
2016-12-24 13:33:21 +03:00
} else {
2022-06-13 12:37:59 +03:00
issueStats , err = issues_model . GetIssueStats ( & issues_model . IssueStatsOptions {
2021-01-17 19:34:19 +03:00
RepoID : repo . ID ,
Labels : selectLabels ,
MilestoneID : milestoneID ,
2023-01-29 06:45:29 +03:00
ProjectID : projectID ,
2021-01-17 19:34:19 +03:00
AssigneeID : assigneeID ,
MentionedID : mentionedID ,
PosterID : posterID ,
ReviewRequestedID : reviewRequestedID ,
2023-02-25 05:55:50 +03:00
ReviewedID : reviewedID ,
2021-01-17 19:34:19 +03:00
IsPull : isPullOption ,
IssueIDs : issueIDs ,
2016-12-24 13:33:21 +03:00
} )
2017-01-25 05:43:02 +03:00
if err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "GetIssueStats" , err )
2017-01-25 05:43:02 +03:00
return
}
2016-12-24 13:33:21 +03:00
}
2020-07-10 00:13:06 +03:00
2021-08-11 03:31:13 +03:00
isShowClosed := ctx . FormString ( "state" ) == "closed"
2020-07-10 00:13:06 +03:00
// if open issues are zero and close don't, use closed as default
2021-08-11 03:31:13 +03:00
if len ( ctx . FormString ( "state" ) ) == 0 && issueStats . OpenCount == 0 && issueStats . ClosedCount != 0 {
2020-07-10 00:13:06 +03:00
isShowClosed = true
}
2021-07-29 04:42:15 +03:00
page := ctx . FormInt ( "page" )
2015-07-24 11:42:47 +03:00
if page <= 1 {
page = 1
}
2015-07-27 22:14:37 +03:00
var total int
if ! isShowClosed {
total = int ( issueStats . OpenCount )
} else {
total = int ( issueStats . ClosedCount )
2015-07-24 11:42:47 +03:00
}
2019-04-20 07:15:19 +03:00
pager := context . NewPagination ( total , setting . UI . IssuePagingNum , page , 5 )
2014-07-26 10:28:04 +04:00
2020-04-30 07:15:39 +03:00
var mileIDs [ ] int64
2023-04-30 16:12:49 +03:00
if milestoneID > 0 || milestoneID == db . NoConditionID { // -1 to get those issues which have no any milestone assigned
2020-04-30 07:15:39 +03:00
mileIDs = [ ] int64 { milestoneID }
}
2022-06-13 12:37:59 +03:00
var issues [ ] * issues_model . Issue
2016-12-24 13:33:21 +03:00
if forceEmpty {
2022-06-13 12:37:59 +03:00
issues = [ ] * issues_model . Issue { }
2016-12-24 13:33:21 +03:00
} else {
2022-11-19 11:12:33 +03:00
issues , err = issues_model . Issues ( ctx , & issues_model . IssuesOptions {
2021-09-24 14:32:56 +03:00
ListOptions : db . ListOptions {
2020-01-24 22:00:29 +03:00
Page : pager . Paginater . Current ( ) ,
PageSize : setting . UI . IssuePagingNum ,
} ,
2022-04-25 17:06:24 +03:00
RepoID : repo . ID ,
2021-01-17 19:34:19 +03:00
AssigneeID : assigneeID ,
PosterID : posterID ,
MentionedID : mentionedID ,
ReviewRequestedID : reviewRequestedID ,
2023-02-25 05:55:50 +03:00
ReviewedID : reviewedID ,
2021-01-17 19:34:19 +03:00
MilestoneIDs : mileIDs ,
ProjectID : projectID ,
IsClosed : util . OptionalBoolOf ( isShowClosed ) ,
IsPull : isPullOption ,
LabelIDs : labelIDs ,
SortType : sortType ,
IssueIDs : issueIDs ,
2016-12-24 13:33:21 +03:00
} )
if err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "Issues" , err )
2016-12-24 13:33:21 +03:00
return
}
2014-07-26 10:28:04 +04:00
}
2022-06-13 12:37:59 +03:00
issueList := issues_model . IssueList ( issues )
2022-05-20 17:08:52 +03:00
approvalCounts , err := issueList . GetApprovalCounts ( ctx )
2020-03-06 06:44:06 +03:00
if err != nil {
ctx . ServerError ( "ApprovalCounts" , err )
return
}
2014-07-26 10:28:04 +04:00
// Get posters.
for i := range issues {
2017-02-03 10:22:39 +03:00
// Check read status
2015-07-24 21:52:25 +03:00
if ! ctx . IsSigned {
issues [ i ] . IsRead = true
2022-03-22 10:03:22 +03:00
} else if err = issues [ i ] . GetIsRead ( ctx . Doer . ID ) ; err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "GetIsRead" , err )
2017-02-03 10:22:39 +03:00
return
2014-07-26 10:28:04 +04:00
}
2021-04-15 20:34:43 +03:00
}
2019-04-02 22:54:29 +03:00
2022-04-27 01:40:01 +03:00
commitStatuses , lastStatus , err := pull_service . GetIssuesAllCommitStatus ( ctx , issues )
2021-04-15 20:34:43 +03:00
if err != nil {
2022-04-27 01:40:01 +03:00
ctx . ServerError ( "GetIssuesAllCommitStatus" , err )
2021-04-15 20:34:43 +03:00
return
2014-07-26 10:28:04 +04:00
}
2019-04-02 22:54:29 +03:00
2015-08-05 15:23:08 +03:00
ctx . Data [ "Issues" ] = issues
2022-04-27 01:40:01 +03:00
ctx . Data [ "CommitLastStatus" ] = lastStatus
ctx . Data [ "CommitStatuses" ] = commitStatuses
2015-08-05 15:23:08 +03:00
2018-11-29 04:46:30 +03:00
// Get assignees.
2023-04-07 03:11:02 +03:00
assigneeUsers , err := repo_model . GetRepoAssignees ( ctx , repo )
2015-08-05 15:23:08 +03:00
if err != nil {
2023-04-07 03:11:02 +03:00
ctx . ServerError ( "GetRepoAssignees" , err )
2022-08-08 23:03:58 +03:00
return
}
2023-04-07 03:11:02 +03:00
ctx . Data [ "Assignees" ] = makeSelfOnTop ( ctx , assigneeUsers )
2022-08-08 23:03:58 +03:00
2020-12-21 18:39:28 +03:00
handleTeamMentions ( ctx )
if ctx . Written ( ) {
return
}
2022-06-13 12:37:59 +03:00
labels , err := issues_model . GetLabelsByRepoID ( ctx , repo . ID , "" , db . ListOptions { } )
2015-08-15 06:24:41 +03:00
if err != nil {
2018-11-29 04:46:30 +03:00
ctx . ServerError ( "GetLabelsByRepoID" , err )
2015-08-15 06:24:41 +03:00
return
}
Add Organization Wide Labels (#10814)
* Add organization wide labels
Implement organization wide labels similar to organization wide
webhooks. This lets you create individual labels for organizations that can be used
for all repos under that organization (so being able to reuse the same
label across multiple repos).
This makes it possible for small organizations with many repos to use
labels effectively.
Fixes #7406
* Add migration
* remove comments
* fix tests
* Update options/locale/locale_en-US.ini
Removed unused translation string
* show org labels in issue search label filter
* Use more clear var name
* rename migration after merge from master
* comment typo
* update migration again after rebase with master
* check for orgID <=0 per guillep2k review
* fmt
* Apply suggestions from code review
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* remove unused code
* Make sure RepoID is 0 when searching orgID per code review
* more changes/code review requests
* More descriptive translation var per code review
* func description/delete comment when issue label deleted instead of hiding it
* remove comment
* only use issues in that repo when calculating number of open issues for org label on repo label page
* Add integration test for IssuesSearch API with labels
* remove unused function
* Update models/issue_label.go
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* Use subquery in GetLabelIDsInReposByNames
* Fix tests to use correct orgID
* fix more tests
* IssuesSearch api now uses new BuildLabelNamesIssueIDsCondition. Add a few more tests as well
* update comment for clarity
* Revert previous code change now that we can use the new BuildLabelNamesIssueIDsCondition
* Don't sort repos by date in IssuesSearch API
After much debugging I've found a strange issue where in some cases MySQL will return a different result than other enigines if a query is sorted by a null collumn. For example with our integration test data where we don't set updated_unix in repository fixtures:
SELECT `id`, `owner_id`, `owner_name`, `lower_name`, `name`, `description`, `website`, `original_service_type`, `original_url`, `default_branch`, `num_watches`, `num_stars`, `num_forks`, `num_issues`, `num_closed_issues`, `num_pulls`, `num_closed_pulls`, `num_milestones`, `num_closed_milestones`, `is_private`, `is_empty`, `is_archived`, `is_mirror`, `status`, `is_fork`, `fork_id`, `is_template`, `template_id`, `size`, `is_fsck_enabled`, `close_issues_via_commit_in_any_branch`, `topics`, `avatar`, `created_unix`, `updated_unix` FROM `repository` ORDER BY updated_unix DESC LIMIT 15 OFFSET 45
Returns different results for MySQL than other engines. However, the similar query:
SELECT `id`, `owner_id`, `owner_name`, `lower_name`, `name`, `description`, `website`, `original_service_type`, `original_url`, `default_branch`, `num_watches`, `num_stars`, `num_forks`, `num_issues`, `num_closed_issues`, `num_pulls`, `num_closed_pulls`, `num_milestones`, `num_closed_milestones`, `is_private`, `is_empty`, `is_archived`, `is_mirror`, `status`, `is_fork`, `fork_id`, `is_template`, `template_id`, `size`, `is_fsck_enabled`, `close_issues_via_commit_in_any_branch`, `topics`, `avatar`, `created_unix`, `updated_unix` FROM `repository` ORDER BY updated_unix DESC LIMIT 15 OFFSET 30
Returns the same results.
This causes integration tests to fail on MySQL in certain cases but would never show up in a real installation. Since this API call always returns issues based on the optionally provided repo_priority_id or the issueID itself, there is no change to results by changing the repo sorting method used to get ids earlier in the function.
* linter is back!
* code review
* remove now unused option
* Fix newline at end of files
* more unused code
* update to master
* check for matching ids before query
* Update models/issue_label.go
Co-Authored-By: 6543 <6543@obermui.de>
* Update models/issue_label.go
* update comments
* Update routers/org/setting.go
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
Co-authored-by: 6543 <6543@obermui.de>
2020-04-01 07:14:46 +03:00
if repo . Owner . IsOrganization ( ) {
2022-06-13 12:37:59 +03:00
orgLabels , err := issues_model . GetLabelsByOrgID ( ctx , repo . Owner . ID , ctx . FormString ( "sort" ) , db . ListOptions { } )
Add Organization Wide Labels (#10814)
* Add organization wide labels
Implement organization wide labels similar to organization wide
webhooks. This lets you create individual labels for organizations that can be used
for all repos under that organization (so being able to reuse the same
label across multiple repos).
This makes it possible for small organizations with many repos to use
labels effectively.
Fixes #7406
* Add migration
* remove comments
* fix tests
* Update options/locale/locale_en-US.ini
Removed unused translation string
* show org labels in issue search label filter
* Use more clear var name
* rename migration after merge from master
* comment typo
* update migration again after rebase with master
* check for orgID <=0 per guillep2k review
* fmt
* Apply suggestions from code review
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* remove unused code
* Make sure RepoID is 0 when searching orgID per code review
* more changes/code review requests
* More descriptive translation var per code review
* func description/delete comment when issue label deleted instead of hiding it
* remove comment
* only use issues in that repo when calculating number of open issues for org label on repo label page
* Add integration test for IssuesSearch API with labels
* remove unused function
* Update models/issue_label.go
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* Use subquery in GetLabelIDsInReposByNames
* Fix tests to use correct orgID
* fix more tests
* IssuesSearch api now uses new BuildLabelNamesIssueIDsCondition. Add a few more tests as well
* update comment for clarity
* Revert previous code change now that we can use the new BuildLabelNamesIssueIDsCondition
* Don't sort repos by date in IssuesSearch API
After much debugging I've found a strange issue where in some cases MySQL will return a different result than other enigines if a query is sorted by a null collumn. For example with our integration test data where we don't set updated_unix in repository fixtures:
SELECT `id`, `owner_id`, `owner_name`, `lower_name`, `name`, `description`, `website`, `original_service_type`, `original_url`, `default_branch`, `num_watches`, `num_stars`, `num_forks`, `num_issues`, `num_closed_issues`, `num_pulls`, `num_closed_pulls`, `num_milestones`, `num_closed_milestones`, `is_private`, `is_empty`, `is_archived`, `is_mirror`, `status`, `is_fork`, `fork_id`, `is_template`, `template_id`, `size`, `is_fsck_enabled`, `close_issues_via_commit_in_any_branch`, `topics`, `avatar`, `created_unix`, `updated_unix` FROM `repository` ORDER BY updated_unix DESC LIMIT 15 OFFSET 45
Returns different results for MySQL than other engines. However, the similar query:
SELECT `id`, `owner_id`, `owner_name`, `lower_name`, `name`, `description`, `website`, `original_service_type`, `original_url`, `default_branch`, `num_watches`, `num_stars`, `num_forks`, `num_issues`, `num_closed_issues`, `num_pulls`, `num_closed_pulls`, `num_milestones`, `num_closed_milestones`, `is_private`, `is_empty`, `is_archived`, `is_mirror`, `status`, `is_fork`, `fork_id`, `is_template`, `template_id`, `size`, `is_fsck_enabled`, `close_issues_via_commit_in_any_branch`, `topics`, `avatar`, `created_unix`, `updated_unix` FROM `repository` ORDER BY updated_unix DESC LIMIT 15 OFFSET 30
Returns the same results.
This causes integration tests to fail on MySQL in certain cases but would never show up in a real installation. Since this API call always returns issues based on the optionally provided repo_priority_id or the issueID itself, there is no change to results by changing the repo sorting method used to get ids earlier in the function.
* linter is back!
* code review
* remove now unused option
* Fix newline at end of files
* more unused code
* update to master
* check for matching ids before query
* Update models/issue_label.go
Co-Authored-By: 6543 <6543@obermui.de>
* Update models/issue_label.go
* update comments
* Update routers/org/setting.go
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
Co-authored-by: 6543 <6543@obermui.de>
2020-04-01 07:14:46 +03:00
if err != nil {
ctx . ServerError ( "GetLabelsByOrgID" , err )
return
}
ctx . Data [ "OrgLabels" ] = orgLabels
labels = append ( labels , orgLabels ... )
}
Scoped labels (#22585)
Add a new "exclusive" option per label. This makes it so that when the
label is named `scope/name`, no other label with the same `scope/`
prefix can be set on an issue.
The scope is determined by the last occurence of `/`, so for example
`scope/alpha/name` and `scope/beta/name` are considered to be in
different scopes and can coexist.
Exclusive scopes are not enforced by any database rules, however they
are enforced when editing labels at the models level, automatically
removing any existing labels in the same scope when either attaching a
new label or replacing all labels.
In menus use a circle instead of checkbox to indicate they function as
radio buttons per scope. Issue filtering by label ensures that only a
single scoped label is selected at a time. Clicking with alt key can be
used to remove a scoped label, both when editing individual issues and
batch editing.
Label rendering refactor for consistency and code simplification:
* Labels now consistently have the same shape, emojis and tooltips
everywhere. This includes the label list and label assignment menus.
* In label list, show description below label same as label menus.
* Don't use exactly black/white text colors to look a bit nicer.
* Simplify text color computation. There is no point computing luminance
in linear color space, as this is a perceptual problem and sRGB is
closer to perceptually linear.
* Increase height of label assignment menus to show more labels. Showing
only 3-4 labels at a time leads to a lot of scrolling.
* Render all labels with a new RenderLabel template helper function.
Label creation and editing in multiline modal menu:
* Change label creation to open a modal menu like label editing.
* Change menu layout to place name, description and colors on separate
lines.
* Don't color cancel button red in label editing modal menu.
* Align text to the left in model menu for better readability and
consistent with settings layout elsewhere.
Custom exclusive scoped label rendering:
* Display scoped label prefix and suffix with slightly darker and
lighter background color respectively, and a slanted edge between them
similar to the `/` symbol.
* In menus exclusive labels are grouped with a divider line.
---------
Co-authored-by: Yarden Shoham <hrsi88@gmail.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
2023-02-18 22:17:39 +03:00
// Get the exclusive scope for every label ID
labelExclusiveScopes := make ( [ ] string , 0 , len ( labelIDs ) )
for _ , labelID := range labelIDs {
foundExclusiveScope := false
for _ , label := range labels {
if label . ID == labelID || label . ID == - labelID {
labelExclusiveScopes = append ( labelExclusiveScopes , label . ExclusiveScope ( ) )
foundExclusiveScope = true
break
}
}
if ! foundExclusiveScope {
labelExclusiveScopes = append ( labelExclusiveScopes , "" )
}
}
2019-01-23 07:10:38 +03:00
for _ , l := range labels {
Scoped labels (#22585)
Add a new "exclusive" option per label. This makes it so that when the
label is named `scope/name`, no other label with the same `scope/`
prefix can be set on an issue.
The scope is determined by the last occurence of `/`, so for example
`scope/alpha/name` and `scope/beta/name` are considered to be in
different scopes and can coexist.
Exclusive scopes are not enforced by any database rules, however they
are enforced when editing labels at the models level, automatically
removing any existing labels in the same scope when either attaching a
new label or replacing all labels.
In menus use a circle instead of checkbox to indicate they function as
radio buttons per scope. Issue filtering by label ensures that only a
single scoped label is selected at a time. Clicking with alt key can be
used to remove a scoped label, both when editing individual issues and
batch editing.
Label rendering refactor for consistency and code simplification:
* Labels now consistently have the same shape, emojis and tooltips
everywhere. This includes the label list and label assignment menus.
* In label list, show description below label same as label menus.
* Don't use exactly black/white text colors to look a bit nicer.
* Simplify text color computation. There is no point computing luminance
in linear color space, as this is a perceptual problem and sRGB is
closer to perceptually linear.
* Increase height of label assignment menus to show more labels. Showing
only 3-4 labels at a time leads to a lot of scrolling.
* Render all labels with a new RenderLabel template helper function.
Label creation and editing in multiline modal menu:
* Change label creation to open a modal menu like label editing.
* Change menu layout to place name, description and colors on separate
lines.
* Don't color cancel button red in label editing modal menu.
* Align text to the left in model menu for better readability and
consistent with settings layout elsewhere.
Custom exclusive scoped label rendering:
* Display scoped label prefix and suffix with slightly darker and
lighter background color respectively, and a slanted edge between them
similar to the `/` symbol.
* In menus exclusive labels are grouped with a divider line.
---------
Co-authored-by: Yarden Shoham <hrsi88@gmail.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
2023-02-18 22:17:39 +03:00
l . LoadSelectedLabelsAfterClick ( labelIDs , labelExclusiveScopes )
2019-01-23 07:10:38 +03:00
}
2018-11-29 04:46:30 +03:00
ctx . Data [ "Labels" ] = labels
2019-01-23 07:10:38 +03:00
ctx . Data [ "NumLabels" ] = len ( labels )
2014-07-26 10:28:04 +04:00
2021-07-29 04:42:15 +03:00
if ctx . FormInt64 ( "assignee" ) == 0 {
2016-07-17 04:25:30 +03:00
assigneeID = 0 // Reset ID to prevent unexpected selection of assignee.
}
2022-01-20 20:46:10 +03:00
ctx . Data [ "IssueRefEndNames" ] , ctx . Data [ "IssueRefURLs" ] = issue_service . GetRefEndNamesAndURLs ( issues , ctx . Repo . RepoLink )
2020-05-15 01:55:43 +03:00
2020-03-06 06:44:06 +03:00
ctx . Data [ "ApprovalCounts" ] = func ( issueID int64 , typ string ) int64 {
counts , ok := approvalCounts [ issueID ]
if ! ok || len ( counts ) == 0 {
return 0
}
2022-06-13 12:37:59 +03:00
reviewTyp := issues_model . ReviewTypeApprove
2020-03-06 06:44:06 +03:00
if typ == "reject" {
2022-06-13 12:37:59 +03:00
reviewTyp = issues_model . ReviewTypeReject
2020-04-06 19:33:34 +03:00
} else if typ == "waiting" {
2022-06-13 12:37:59 +03:00
reviewTyp = issues_model . ReviewTypeRequest
2020-03-06 06:44:06 +03:00
}
for _ , count := range counts {
if count . Type == reviewTyp {
return count . Count
}
}
return 0
}
2021-10-08 01:00:02 +03:00
2023-02-04 17:35:08 +03:00
retrieveProjects ( ctx , repo )
if ctx . Written ( ) {
2023-01-29 06:45:29 +03:00
return
2021-10-08 01:00:02 +03:00
}
2014-07-26 10:28:04 +04:00
ctx . Data [ "IssueStats" ] = issueStats
2019-12-28 17:43:46 +03:00
ctx . Data [ "SelLabelIDs" ] = labelIDs
ctx . Data [ "SelectLabels" ] = selectLabels
2014-07-26 10:28:04 +04:00
ctx . Data [ "ViewType" ] = viewType
2015-08-15 07:07:08 +03:00
ctx . Data [ "SortType" ] = sortType
2015-08-05 15:23:08 +03:00
ctx . Data [ "MilestoneID" ] = milestoneID
2023-01-29 06:45:29 +03:00
ctx . Data [ "ProjectID" ] = projectID
2015-08-15 06:24:41 +03:00
ctx . Data [ "AssigneeID" ] = assigneeID
2022-08-08 23:03:58 +03:00
ctx . Data [ "PosterID" ] = posterID
2014-07-26 10:28:04 +04:00
ctx . Data [ "IsShowClosed" ] = isShowClosed
2017-01-25 05:43:02 +03:00
ctx . Data [ "Keyword" ] = keyword
2014-07-26 10:28:04 +04:00
if isShowClosed {
ctx . Data [ "State" ] = "closed"
} else {
2015-07-24 21:52:25 +03:00
ctx . Data [ "State" ] = "open"
2014-07-26 10:28:04 +04:00
}
2019-04-20 07:15:19 +03:00
pager . AddParam ( ctx , "q" , "Keyword" )
pager . AddParam ( ctx , "type" , "ViewType" )
pager . AddParam ( ctx , "sort" , "SortType" )
pager . AddParam ( ctx , "state" , "State" )
pager . AddParam ( ctx , "labels" , "SelectLabels" )
pager . AddParam ( ctx , "milestone" , "MilestoneID" )
2023-01-29 06:45:29 +03:00
pager . AddParam ( ctx , "project" , "ProjectID" )
2019-04-20 07:15:19 +03:00
pager . AddParam ( ctx , "assignee" , "AssigneeID" )
2022-08-08 23:03:58 +03:00
pager . AddParam ( ctx , "poster" , "PosterID" )
2019-04-20 07:15:19 +03:00
ctx . Data [ "Page" ] = pager
2018-11-29 04:46:30 +03:00
}
// Issues render issues page
func Issues ( ctx * context . Context ) {
isPullList := ctx . Params ( ":type" ) == "pulls"
if isPullList {
MustAllowPulls ( ctx )
if ctx . Written ( ) {
return
}
ctx . Data [ "Title" ] = ctx . Tr ( "repo.pulls" )
ctx . Data [ "PageIsPullList" ] = true
} else {
MustEnableIssues ( ctx )
if ctx . Written ( ) {
return
}
ctx . Data [ "Title" ] = ctx . Tr ( "repo.issues" )
ctx . Data [ "PageIsIssueList" ] = true
2023-03-28 21:22:07 +03:00
ctx . Data [ "NewIssueChooseTemplate" ] = ctx . HasIssueTemplatesOrContactLinks ( )
2018-11-29 04:46:30 +03:00
}
2021-07-29 04:42:15 +03:00
issues ( ctx , ctx . FormInt64 ( "milestone" ) , ctx . FormInt64 ( "project" ) , util . OptionalBoolOf ( isPullList ) )
2021-05-09 05:33:49 +03:00
if ctx . Written ( ) {
return
}
2018-11-29 04:46:30 +03:00
2023-04-30 16:12:49 +03:00
renderMilestones ( ctx )
if ctx . Written ( ) {
return
}
ctx . Data [ "CanWriteIssuesOrPulls" ] = ctx . Repo . CanWriteIssuesOrPulls ( isPullList )
ctx . HTML ( http . StatusOK , tplIssues )
}
func renderMilestones ( ctx * context . Context ) {
2020-07-28 14:30:40 +03:00
// Get milestones
2023-04-30 16:12:49 +03:00
milestones , _ , err := issues_model . GetMilestones ( issues_model . GetMilestonesOption {
2020-07-28 14:30:40 +03:00
RepoID : ctx . Repo . Repository . ID ,
2023-04-30 16:12:49 +03:00
State : api . StateAll ,
2020-07-28 14:30:40 +03:00
} )
2018-11-29 04:46:30 +03:00
if err != nil {
ctx . ServerError ( "GetAllRepoMilestones" , err )
return
}
2015-07-24 21:52:25 +03:00
2023-04-30 16:12:49 +03:00
openMilestones , closedMilestones := issues_model . MilestoneList { } , issues_model . MilestoneList { }
for _ , milestone := range milestones {
if milestone . IsClosed {
closedMilestones = append ( closedMilestones , milestone )
} else {
openMilestones = append ( openMilestones , milestone )
}
}
ctx . Data [ "OpenMilestones" ] = openMilestones
ctx . Data [ "ClosedMilestones" ] = closedMilestones
2014-07-26 10:28:04 +04:00
}
2016-11-24 10:04:31 +03:00
// RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
2021-12-10 04:27:50 +03:00
func RetrieveRepoMilestonesAndAssignees ( ctx * context . Context , repo * repo_model . Repository ) {
2015-09-02 02:07:02 +03:00
var err error
2022-04-08 12:11:15 +03:00
ctx . Data [ "OpenMilestones" ] , _ , err = issues_model . GetMilestones ( issues_model . GetMilestonesOption {
2020-07-28 14:30:40 +03:00
RepoID : repo . ID ,
State : api . StateOpen ,
} )
2015-09-02 02:07:02 +03:00
if err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "GetMilestones" , err )
2015-09-02 02:07:02 +03:00
return
}
2022-04-08 12:11:15 +03:00
ctx . Data [ "ClosedMilestones" ] , _ , err = issues_model . GetMilestones ( issues_model . GetMilestonesOption {
2020-07-28 14:30:40 +03:00
RepoID : repo . ID ,
State : api . StateClosed ,
} )
2015-09-02 02:07:02 +03:00
if err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "GetMilestones" , err )
2015-09-02 02:07:02 +03:00
return
}
2023-04-07 03:11:02 +03:00
assigneeUsers , err := repo_model . GetRepoAssignees ( ctx , repo )
2015-09-02 02:07:02 +03:00
if err != nil {
2023-04-07 03:11:02 +03:00
ctx . ServerError ( "GetRepoAssignees" , err )
2015-09-02 02:07:02 +03:00
return
}
2023-04-07 03:11:02 +03:00
ctx . Data [ "Assignees" ] = makeSelfOnTop ( ctx , assigneeUsers )
2020-12-21 18:39:28 +03:00
handleTeamMentions ( ctx )
2015-09-02 02:07:02 +03:00
}
2021-12-10 04:27:50 +03:00
func retrieveProjects ( ctx * context . Context , repo * repo_model . Repository ) {
2020-08-17 06:07:38 +03:00
var err error
2023-01-20 14:42:33 +03:00
projects , _ , err := project_model . FindProjects ( ctx , project_model . SearchOptions {
2020-08-17 06:07:38 +03:00
RepoID : repo . ID ,
Page : - 1 ,
IsClosed : util . OptionalBoolFalse ,
2022-03-29 17:16:31 +03:00
Type : project_model . TypeRepository ,
2020-08-17 06:07:38 +03:00
} )
if err != nil {
ctx . ServerError ( "GetProjects" , err )
return
}
2023-01-20 14:42:33 +03:00
projects2 , _ , err := project_model . FindProjects ( ctx , project_model . SearchOptions {
OwnerID : repo . OwnerID ,
Page : - 1 ,
IsClosed : util . OptionalBoolFalse ,
Type : project_model . TypeOrganization ,
} )
if err != nil {
ctx . ServerError ( "GetProjects" , err )
return
}
ctx . Data [ "OpenProjects" ] = append ( projects , projects2 ... )
2020-08-17 06:07:38 +03:00
2023-01-20 14:42:33 +03:00
projects , _ , err = project_model . FindProjects ( ctx , project_model . SearchOptions {
2020-08-17 06:07:38 +03:00
RepoID : repo . ID ,
Page : - 1 ,
IsClosed : util . OptionalBoolTrue ,
2022-03-29 17:16:31 +03:00
Type : project_model . TypeRepository ,
2020-08-17 06:07:38 +03:00
} )
if err != nil {
ctx . ServerError ( "GetProjects" , err )
return
}
2023-01-20 14:42:33 +03:00
projects2 , _ , err = project_model . FindProjects ( ctx , project_model . SearchOptions {
OwnerID : repo . OwnerID ,
Page : - 1 ,
IsClosed : util . OptionalBoolTrue ,
Type : project_model . TypeOrganization ,
} )
if err != nil {
ctx . ServerError ( "GetProjects" , err )
return
}
ctx . Data [ "ClosedProjects" ] = append ( projects , projects2 ... )
2020-08-17 06:07:38 +03:00
}
2020-10-12 22:55:13 +03:00
// repoReviewerSelection items to bee shown
type repoReviewerSelection struct {
IsTeam bool
2022-03-29 09:29:02 +03:00
Team * organization . Team
2021-11-24 12:49:20 +03:00
User * user_model . User
2022-06-13 12:37:59 +03:00
Review * issues_model . Review
2020-10-12 22:55:13 +03:00
CanChange bool
Checked bool
ItemID int64
}
2020-04-06 19:33:34 +03:00
// RetrieveRepoReviewers find all reviewers of a repository
2022-06-13 12:37:59 +03:00
func RetrieveRepoReviewers ( ctx * context . Context , repo * repo_model . Repository , issue * issues_model . Issue , canChooseReviewer bool ) {
2020-10-12 22:55:13 +03:00
ctx . Data [ "CanChooseReviewer" ] = canChooseReviewer
2022-06-13 12:37:59 +03:00
originalAuthorReviews , err := issues_model . GetReviewersFromOriginalAuthorsByIssueID ( issue . ID )
2020-10-14 15:11:11 +03:00
if err != nil {
ctx . ServerError ( "GetReviewersFromOriginalAuthorsByIssueID" , err )
return
}
ctx . Data [ "OriginalReviews" ] = originalAuthorReviews
2022-06-13 12:37:59 +03:00
reviews , err := issues_model . GetReviewersByIssueID ( issue . ID )
2020-04-06 19:33:34 +03:00
if err != nil {
2020-10-12 22:55:13 +03:00
ctx . ServerError ( "GetReviewersByIssueID" , err )
return
}
if len ( reviews ) == 0 && ! canChooseReviewer {
2020-04-06 19:33:34 +03:00
return
}
2020-10-12 22:55:13 +03:00
var (
pullReviews [ ] * repoReviewerSelection
reviewersResult [ ] * repoReviewerSelection
teamReviewersResult [ ] * repoReviewerSelection
2022-03-29 09:29:02 +03:00
teamReviewers [ ] * organization . Team
2021-11-24 12:49:20 +03:00
reviewers [ ] * user_model . User
2020-10-12 22:55:13 +03:00
)
if canChooseReviewer {
posterID := issue . PosterID
if issue . OriginalAuthorID > 0 {
posterID = 0
}
2022-06-06 11:01:49 +03:00
reviewers , err = repo_model . GetReviewers ( ctx , repo , ctx . Doer . ID , posterID )
2020-10-12 22:55:13 +03:00
if err != nil {
ctx . ServerError ( "GetReviewers" , err )
return
}
2023-03-01 01:17:51 +03:00
teamReviewers , err = repo_service . GetReviewerTeams ( ctx , repo )
2020-10-12 22:55:13 +03:00
if err != nil {
ctx . ServerError ( "GetReviewerTeams" , err )
return
}
if len ( reviewers ) > 0 {
reviewersResult = make ( [ ] * repoReviewerSelection , 0 , len ( reviewers ) )
}
if len ( teamReviewers ) > 0 {
teamReviewersResult = make ( [ ] * repoReviewerSelection , 0 , len ( teamReviewers ) )
}
}
pullReviews = make ( [ ] * repoReviewerSelection , 0 , len ( reviews ) )
for _ , review := range reviews {
tmp := & repoReviewerSelection {
2022-06-13 12:37:59 +03:00
Checked : review . Type == issues_model . ReviewTypeRequest ,
2020-10-12 22:55:13 +03:00
Review : review ,
ItemID : review . ReviewerID ,
}
if review . ReviewerTeamID > 0 {
tmp . IsTeam = true
tmp . ItemID = - review . ReviewerTeamID
}
if ctx . Repo . IsAdmin ( ) {
// Admin can dismiss or re-request any review requests
tmp . CanChange = true
2022-06-13 12:37:59 +03:00
} else if ctx . Doer != nil && ctx . Doer . ID == review . ReviewerID && review . Type == issues_model . ReviewTypeRequest {
2020-10-12 22:55:13 +03:00
// A user can refuse review requests
tmp . CanChange = true
2022-06-13 12:37:59 +03:00
} else if ( canChooseReviewer || ( ctx . Doer != nil && ctx . Doer . ID == issue . PosterID ) ) && review . Type != issues_model . ReviewTypeRequest &&
2022-03-22 10:03:22 +03:00
ctx . Doer . ID != review . ReviewerID {
2020-10-12 22:55:13 +03:00
// The poster of the PR, a manager, or official reviewers can re-request review from other reviewers
tmp . CanChange = true
}
pullReviews = append ( pullReviews , tmp )
if canChooseReviewer {
if tmp . IsTeam {
teamReviewersResult = append ( teamReviewersResult , tmp )
} else {
reviewersResult = append ( reviewersResult , tmp )
}
}
}
if len ( pullReviews ) > 0 {
// Drop all non-existing users and teams from the reviews
currentPullReviewers := make ( [ ] * repoReviewerSelection , 0 , len ( pullReviews ) )
for _ , item := range pullReviews {
if item . Review . ReviewerID > 0 {
2022-11-19 11:12:33 +03:00
if err = item . Review . LoadReviewer ( ctx ) ; err != nil {
2021-11-24 12:49:20 +03:00
if user_model . IsErrUserNotExist ( err ) {
2020-10-12 22:55:13 +03:00
continue
}
ctx . ServerError ( "LoadReviewer" , err )
return
}
item . User = item . Review . Reviewer
} else if item . Review . ReviewerTeamID > 0 {
2022-11-19 11:12:33 +03:00
if err = item . Review . LoadReviewerTeam ( ctx ) ; err != nil {
2022-03-29 09:29:02 +03:00
if organization . IsErrTeamNotExist ( err ) {
2020-10-12 22:55:13 +03:00
continue
}
ctx . ServerError ( "LoadReviewerTeam" , err )
return
}
item . Team = item . Review . ReviewerTeam
} else {
continue
}
currentPullReviewers = append ( currentPullReviewers , item )
}
ctx . Data [ "PullReviewers" ] = currentPullReviewers
}
if canChooseReviewer && reviewersResult != nil {
preadded := len ( reviewersResult )
for _ , reviewer := range reviewers {
found := false
reviewAddLoop :
for _ , tmp := range reviewersResult [ : preadded ] {
if tmp . ItemID == reviewer . ID {
tmp . User = reviewer
found = true
break reviewAddLoop
}
}
if found {
continue
}
reviewersResult = append ( reviewersResult , & repoReviewerSelection {
IsTeam : false ,
CanChange : true ,
User : reviewer ,
ItemID : reviewer . ID ,
} )
}
ctx . Data [ "Reviewers" ] = reviewersResult
}
if canChooseReviewer && teamReviewersResult != nil {
preadded := len ( teamReviewersResult )
for _ , team := range teamReviewers {
found := false
teamReviewAddLoop :
for _ , tmp := range teamReviewersResult [ : preadded ] {
if tmp . ItemID == - team . ID {
tmp . Team = team
found = true
break teamReviewAddLoop
}
}
if found {
continue
}
teamReviewersResult = append ( teamReviewersResult , & repoReviewerSelection {
IsTeam : true ,
CanChange : true ,
Team : team ,
ItemID : - team . ID ,
} )
}
ctx . Data [ "TeamReviewers" ] = teamReviewersResult
}
2020-04-06 19:33:34 +03:00
}
2016-11-24 10:04:31 +03:00
// RetrieveRepoMetas find all the meta information of a repository
2022-06-13 12:37:59 +03:00
func RetrieveRepoMetas ( ctx * context . Context , repo * repo_model . Repository , isPull bool ) [ ] * issues_model . Label {
2020-01-20 15:00:32 +03:00
if ! ctx . Repo . CanWriteIssuesOrPulls ( isPull ) {
2015-08-31 10:24:28 +03:00
return nil
}
2022-06-13 12:37:59 +03:00
labels , err := issues_model . GetLabelsByRepoID ( ctx , repo . ID , "" , db . ListOptions { } )
2015-08-31 10:24:28 +03:00
if err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "GetLabelsByRepoID" , err )
2015-08-31 10:24:28 +03:00
return nil
}
ctx . Data [ "Labels" ] = labels
Add Organization Wide Labels (#10814)
* Add organization wide labels
Implement organization wide labels similar to organization wide
webhooks. This lets you create individual labels for organizations that can be used
for all repos under that organization (so being able to reuse the same
label across multiple repos).
This makes it possible for small organizations with many repos to use
labels effectively.
Fixes #7406
* Add migration
* remove comments
* fix tests
* Update options/locale/locale_en-US.ini
Removed unused translation string
* show org labels in issue search label filter
* Use more clear var name
* rename migration after merge from master
* comment typo
* update migration again after rebase with master
* check for orgID <=0 per guillep2k review
* fmt
* Apply suggestions from code review
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* remove unused code
* Make sure RepoID is 0 when searching orgID per code review
* more changes/code review requests
* More descriptive translation var per code review
* func description/delete comment when issue label deleted instead of hiding it
* remove comment
* only use issues in that repo when calculating number of open issues for org label on repo label page
* Add integration test for IssuesSearch API with labels
* remove unused function
* Update models/issue_label.go
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* Use subquery in GetLabelIDsInReposByNames
* Fix tests to use correct orgID
* fix more tests
* IssuesSearch api now uses new BuildLabelNamesIssueIDsCondition. Add a few more tests as well
* update comment for clarity
* Revert previous code change now that we can use the new BuildLabelNamesIssueIDsCondition
* Don't sort repos by date in IssuesSearch API
After much debugging I've found a strange issue where in some cases MySQL will return a different result than other enigines if a query is sorted by a null collumn. For example with our integration test data where we don't set updated_unix in repository fixtures:
SELECT `id`, `owner_id`, `owner_name`, `lower_name`, `name`, `description`, `website`, `original_service_type`, `original_url`, `default_branch`, `num_watches`, `num_stars`, `num_forks`, `num_issues`, `num_closed_issues`, `num_pulls`, `num_closed_pulls`, `num_milestones`, `num_closed_milestones`, `is_private`, `is_empty`, `is_archived`, `is_mirror`, `status`, `is_fork`, `fork_id`, `is_template`, `template_id`, `size`, `is_fsck_enabled`, `close_issues_via_commit_in_any_branch`, `topics`, `avatar`, `created_unix`, `updated_unix` FROM `repository` ORDER BY updated_unix DESC LIMIT 15 OFFSET 45
Returns different results for MySQL than other engines. However, the similar query:
SELECT `id`, `owner_id`, `owner_name`, `lower_name`, `name`, `description`, `website`, `original_service_type`, `original_url`, `default_branch`, `num_watches`, `num_stars`, `num_forks`, `num_issues`, `num_closed_issues`, `num_pulls`, `num_closed_pulls`, `num_milestones`, `num_closed_milestones`, `is_private`, `is_empty`, `is_archived`, `is_mirror`, `status`, `is_fork`, `fork_id`, `is_template`, `template_id`, `size`, `is_fsck_enabled`, `close_issues_via_commit_in_any_branch`, `topics`, `avatar`, `created_unix`, `updated_unix` FROM `repository` ORDER BY updated_unix DESC LIMIT 15 OFFSET 30
Returns the same results.
This causes integration tests to fail on MySQL in certain cases but would never show up in a real installation. Since this API call always returns issues based on the optionally provided repo_priority_id or the issueID itself, there is no change to results by changing the repo sorting method used to get ids earlier in the function.
* linter is back!
* code review
* remove now unused option
* Fix newline at end of files
* more unused code
* update to master
* check for matching ids before query
* Update models/issue_label.go
Co-Authored-By: 6543 <6543@obermui.de>
* Update models/issue_label.go
* update comments
* Update routers/org/setting.go
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
Co-authored-by: 6543 <6543@obermui.de>
2020-04-01 07:14:46 +03:00
if repo . Owner . IsOrganization ( ) {
2022-06-13 12:37:59 +03:00
orgLabels , err := issues_model . GetLabelsByOrgID ( ctx , repo . Owner . ID , ctx . FormString ( "sort" ) , db . ListOptions { } )
Add Organization Wide Labels (#10814)
* Add organization wide labels
Implement organization wide labels similar to organization wide
webhooks. This lets you create individual labels for organizations that can be used
for all repos under that organization (so being able to reuse the same
label across multiple repos).
This makes it possible for small organizations with many repos to use
labels effectively.
Fixes #7406
* Add migration
* remove comments
* fix tests
* Update options/locale/locale_en-US.ini
Removed unused translation string
* show org labels in issue search label filter
* Use more clear var name
* rename migration after merge from master
* comment typo
* update migration again after rebase with master
* check for orgID <=0 per guillep2k review
* fmt
* Apply suggestions from code review
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* remove unused code
* Make sure RepoID is 0 when searching orgID per code review
* more changes/code review requests
* More descriptive translation var per code review
* func description/delete comment when issue label deleted instead of hiding it
* remove comment
* only use issues in that repo when calculating number of open issues for org label on repo label page
* Add integration test for IssuesSearch API with labels
* remove unused function
* Update models/issue_label.go
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* Use subquery in GetLabelIDsInReposByNames
* Fix tests to use correct orgID
* fix more tests
* IssuesSearch api now uses new BuildLabelNamesIssueIDsCondition. Add a few more tests as well
* update comment for clarity
* Revert previous code change now that we can use the new BuildLabelNamesIssueIDsCondition
* Don't sort repos by date in IssuesSearch API
After much debugging I've found a strange issue where in some cases MySQL will return a different result than other enigines if a query is sorted by a null collumn. For example with our integration test data where we don't set updated_unix in repository fixtures:
SELECT `id`, `owner_id`, `owner_name`, `lower_name`, `name`, `description`, `website`, `original_service_type`, `original_url`, `default_branch`, `num_watches`, `num_stars`, `num_forks`, `num_issues`, `num_closed_issues`, `num_pulls`, `num_closed_pulls`, `num_milestones`, `num_closed_milestones`, `is_private`, `is_empty`, `is_archived`, `is_mirror`, `status`, `is_fork`, `fork_id`, `is_template`, `template_id`, `size`, `is_fsck_enabled`, `close_issues_via_commit_in_any_branch`, `topics`, `avatar`, `created_unix`, `updated_unix` FROM `repository` ORDER BY updated_unix DESC LIMIT 15 OFFSET 45
Returns different results for MySQL than other engines. However, the similar query:
SELECT `id`, `owner_id`, `owner_name`, `lower_name`, `name`, `description`, `website`, `original_service_type`, `original_url`, `default_branch`, `num_watches`, `num_stars`, `num_forks`, `num_issues`, `num_closed_issues`, `num_pulls`, `num_closed_pulls`, `num_milestones`, `num_closed_milestones`, `is_private`, `is_empty`, `is_archived`, `is_mirror`, `status`, `is_fork`, `fork_id`, `is_template`, `template_id`, `size`, `is_fsck_enabled`, `close_issues_via_commit_in_any_branch`, `topics`, `avatar`, `created_unix`, `updated_unix` FROM `repository` ORDER BY updated_unix DESC LIMIT 15 OFFSET 30
Returns the same results.
This causes integration tests to fail on MySQL in certain cases but would never show up in a real installation. Since this API call always returns issues based on the optionally provided repo_priority_id or the issueID itself, there is no change to results by changing the repo sorting method used to get ids earlier in the function.
* linter is back!
* code review
* remove now unused option
* Fix newline at end of files
* more unused code
* update to master
* check for matching ids before query
* Update models/issue_label.go
Co-Authored-By: 6543 <6543@obermui.de>
* Update models/issue_label.go
* update comments
* Update routers/org/setting.go
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
Co-authored-by: 6543 <6543@obermui.de>
2020-04-01 07:14:46 +03:00
if err != nil {
return nil
}
ctx . Data [ "OrgLabels" ] = orgLabels
labels = append ( labels , orgLabels ... )
}
2015-08-31 10:24:28 +03:00
2015-09-02 02:07:02 +03:00
RetrieveRepoMilestonesAndAssignees ( ctx , repo )
if ctx . Written ( ) {
2015-08-31 10:24:28 +03:00
return nil
}
2020-08-17 06:07:38 +03:00
retrieveProjects ( ctx , repo )
if ctx . Written ( ) {
return nil
}
2021-12-08 22:08:16 +03:00
brs , _ , err := ctx . Repo . GitRepo . GetBranchNames ( 0 , 0 )
2017-08-24 15:30:27 +03:00
if err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "GetBranches" , err )
2017-08-24 15:30:27 +03:00
return nil
}
ctx . Data [ "Branches" ] = brs
2018-07-18 00:23:58 +03:00
// Contains true if the user can create issue dependencies
2022-03-22 10:03:22 +03:00
ctx . Data [ "CanCreateIssueDependencies" ] = ctx . Repo . CanCreateIssueDependencies ( ctx . Doer , isPull )
2018-07-18 00:23:58 +03:00
2015-08-31 10:24:28 +03:00
return labels
}
2022-09-02 10:58:49 +03:00
func setTemplateIfExists ( ctx * context . Context , ctxDataKey string , possibleFiles [ ] string ) map [ string ] error {
commit , err := ctx . Repo . GitRepo . GetBranchCommit ( ctx . Repo . Repository . DefaultBranch )
2016-02-18 01:21:31 +03:00
if err != nil {
2022-09-02 10:58:49 +03:00
return nil
2016-02-18 01:21:31 +03:00
}
2022-09-02 10:58:49 +03:00
templateCandidates := make ( [ ] string , 0 , 1 + len ( possibleFiles ) )
if t := ctx . FormString ( "template" ) ; t != "" {
templateCandidates = append ( templateCandidates , t )
2020-09-11 17:48:39 +03:00
}
templateCandidates = append ( templateCandidates , possibleFiles ... ) // Append files to the end because they should be fallback
2022-09-02 10:58:49 +03:00
templateErrs := map [ string ] error { }
2020-09-11 17:48:39 +03:00
for _ , filename := range templateCandidates {
2022-09-02 10:58:49 +03:00
if ok , _ := commit . HasFile ( filename ) ; ! ok {
continue
}
template , err := issue_template . UnmarshalFromCommit ( commit , filename )
if err != nil {
templateErrs [ filename ] = err
continue
}
ctx . Data [ issueTemplateTitleKey ] = template . Title
ctx . Data [ ctxDataKey ] = template . Content
if template . Type ( ) == api . IssueTemplateTypeYaml {
2023-01-30 07:36:04 +03:00
// Replace field default values by values from query
for _ , field := range template . Fields {
fieldValue := ctx . FormString ( "field:" + field . ID )
if fieldValue != "" {
field . Attributes [ "value" ] = fieldValue
}
}
2022-09-02 10:58:49 +03:00
ctx . Data [ "Fields" ] = template . Fields
ctx . Data [ "TemplateFile" ] = template . FileName
}
labelIDs := make ( [ ] string , 0 , len ( template . Labels ) )
if repoLabels , err := issues_model . GetLabelsByRepoID ( ctx , ctx . Repo . Repository . ID , "" , db . ListOptions { } ) ; err == nil {
ctx . Data [ "Labels" ] = repoLabels
if ctx . Repo . Owner . IsOrganization ( ) {
if orgLabels , err := issues_model . GetLabelsByOrgID ( ctx , ctx . Repo . Owner . ID , ctx . FormString ( "sort" ) , db . ListOptions { } ) ; err == nil {
ctx . Data [ "OrgLabels" ] = orgLabels
repoLabels = append ( repoLabels , orgLabels ... )
2021-02-10 20:18:22 +03:00
}
2022-09-02 10:58:49 +03:00
}
2021-02-10 20:18:22 +03:00
2022-09-02 10:58:49 +03:00
for _ , metaLabel := range template . Labels {
for _ , repoLabel := range repoLabels {
if strings . EqualFold ( repoLabel . Name , metaLabel ) {
repoLabel . IsChecked = true
labelIDs = append ( labelIDs , strconv . FormatInt ( repoLabel . ID , 10 ) )
break
2020-09-11 17:48:39 +03:00
}
}
}
2022-11-22 15:58:49 +03:00
}
2023-01-19 00:50:22 +03:00
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>
2022-11-22 15:58:49 +03:00
template . Ref = git . BranchPrefix + template . Ref
2016-02-18 01:21:31 +03:00
}
2022-09-02 10:58:49 +03:00
ctx . Data [ "HasSelectedLabel" ] = len ( labelIDs ) > 0
ctx . Data [ "label_ids" ] = strings . Join ( labelIDs , "," )
ctx . Data [ "Reference" ] = template . Ref
ctx . Data [ "RefEndName" ] = git . RefEndName ( template . Ref )
return templateErrs
2016-02-18 01:21:31 +03:00
}
2022-09-02 10:58:49 +03:00
return templateErrs
2016-02-18 01:21:31 +03:00
}
2019-01-21 14:45:32 +03:00
// NewIssue render creating issue page
2016-03-11 19:56:52 +03:00
func NewIssue ( ctx * context . Context ) {
2015-08-09 10:23:02 +03:00
ctx . Data [ "Title" ] = ctx . Tr ( "repo.issues.new" )
ctx . Data [ "PageIsIssueList" ] = true
2023-03-28 21:22:07 +03:00
ctx . Data [ "NewIssueChooseTemplate" ] = ctx . HasIssueTemplatesOrContactLinks ( )
2018-08-13 22:04:39 +03:00
ctx . Data [ "PullRequestWorkInProgressPrefixes" ] = setting . Repository . PullRequest . WorkInProgressPrefixes
2021-08-11 03:31:13 +03:00
title := ctx . FormString ( "title" )
2020-09-11 17:48:39 +03:00
ctx . Data [ "TitleQuery" ] = title
2021-08-11 03:31:13 +03:00
body := ctx . FormString ( "body" )
2019-01-28 18:23:59 +03:00
ctx . Data [ "BodyQuery" ] = body
2021-03-18 05:02:38 +03:00
2022-06-30 18:55:08 +03:00
isProjectsEnabled := ctx . Repo . CanRead ( unit . TypeProjects )
ctx . Data [ "IsProjectsEnabled" ] = isProjectsEnabled
2020-10-05 08:49:33 +03:00
ctx . Data [ "IsAttachmentEnabled" ] = setting . Attachment . Enabled
upload . AddUploadContext ( ctx , "comment" )
2018-11-29 04:46:30 +03:00
2021-07-29 04:42:15 +03:00
milestoneID := ctx . FormInt64 ( "milestone" )
2019-06-10 17:16:02 +03:00
if milestoneID > 0 {
2022-04-08 12:11:15 +03:00
milestone , err := issues_model . GetMilestoneByRepoID ( ctx , ctx . Repo . Repository . ID , milestoneID )
2019-06-10 17:16:02 +03:00
if err != nil {
log . Error ( "GetMilestoneByID: %d: %v" , milestoneID , err )
} else {
ctx . Data [ "milestone_id" ] = milestoneID
ctx . Data [ "Milestone" ] = milestone
}
2018-11-29 04:46:30 +03:00
}
2021-07-29 04:42:15 +03:00
projectID := ctx . FormInt64 ( "project" )
2022-06-30 18:55:08 +03:00
if projectID > 0 && isProjectsEnabled {
2022-05-20 17:08:52 +03:00
project , err := project_model . GetProjectByID ( ctx , projectID )
2020-08-17 06:07:38 +03:00
if err != nil {
log . Error ( "GetProjectByID: %d: %v" , projectID , err )
} else if project . RepoID != ctx . Repo . Repository . ID {
log . Error ( "GetProjectByID: %d: %v" , projectID , fmt . Errorf ( "project[%d] not in repo [%d]" , project . ID , ctx . Repo . Repository . ID ) )
} else {
ctx . Data [ "project_id" ] = projectID
ctx . Data [ "Project" ] = project
}
2021-10-05 22:21:52 +03:00
if len ( ctx . Req . URL . Query ( ) . Get ( "project" ) ) > 0 {
ctx . Data [ "redirect_after_creation" ] = "project"
}
2020-08-17 06:07:38 +03:00
}
2020-01-19 09:43:38 +03:00
RetrieveRepoMetas ( ctx , ctx . Repo . Repository , false )
2022-09-02 10:58:49 +03:00
_ , templateErrs := ctx . IssueTemplatesErrorsFromDefaultBranch ( )
if errs := setTemplateIfExists ( ctx , issueTemplateKey , IssueTemplateCandidates ) ; len ( errs ) > 0 {
for k , v := range errs {
templateErrs [ k ] = v
}
}
2015-08-31 10:24:28 +03:00
if ctx . Written ( ) {
return
2015-08-10 13:57:57 +03:00
}
2015-08-09 10:23:02 +03:00
2022-09-02 10:58:49 +03:00
if len ( templateErrs ) > 0 {
ctx . Flash . Warning ( renderErrorOfTemplates ( ctx , templateErrs ) , true )
}
2021-11-09 22:57:58 +03:00
ctx . Data [ "HasIssuesOrPullsWritePermission" ] = ctx . Repo . CanWrite ( unit . TypeIssues )
2020-04-04 08:39:48 +03:00
2021-04-05 18:30:52 +03:00
ctx . HTML ( http . StatusOK , tplIssueNew )
2014-07-26 10:28:04 +04:00
}
2022-09-02 10:58:49 +03:00
func renderErrorOfTemplates ( ctx * context . Context , errs map [ string ] error ) string {
var files [ ] string
for k := range errs {
files = append ( files , k )
}
sort . Strings ( files ) // keep the output stable
var lines [ ] string
for _ , file := range files {
lines = append ( lines , fmt . Sprintf ( "%s: %v" , file , errs [ file ] ) )
}
flashError , err := ctx . RenderToString ( tplAlertDetails , map [ string ] interface { } {
"Message" : ctx . Tr ( "repo.issues.choose.ignore_invalid_templates" ) ,
"Summary" : ctx . Tr ( "repo.issues.choose.invalid_templates" , len ( errs ) ) ,
"Details" : utils . SanitizeFlashErrorString ( strings . Join ( lines , "\n" ) ) ,
} )
if err != nil {
log . Debug ( "render flash error: %v" , err )
flashError = ctx . Tr ( "repo.issues.choose.ignore_invalid_templates" )
}
return flashError
}
2020-09-11 17:48:39 +03:00
// NewIssueChooseTemplate render creating issue from template page
func NewIssueChooseTemplate ( ctx * context . Context ) {
ctx . Data [ "Title" ] = ctx . Tr ( "repo.issues.new" )
ctx . Data [ "PageIsIssueList" ] = true
2022-09-02 10:58:49 +03:00
issueTemplates , errs := ctx . IssueTemplatesErrorsFromDefaultBranch ( )
2020-09-11 17:48:39 +03:00
ctx . Data [ "IssueTemplates" ] = issueTemplates
2022-09-02 10:58:49 +03:00
if len ( errs ) > 0 {
ctx . Flash . Warning ( renderErrorOfTemplates ( ctx , errs ) , true )
}
2023-03-28 21:22:07 +03:00
if ! ctx . HasIssueTemplatesOrContactLinks ( ) {
2022-03-22 22:54:24 +03:00
// 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.
2023-02-11 09:34:11 +03:00
ctx . Redirect ( fmt . Sprintf ( "%s/issues/new?%s" , ctx . Repo . Repository . Link ( ) , ctx . Req . URL . RawQuery ) , http . StatusSeeOther )
2022-03-22 22:54:24 +03:00
return
}
2023-03-28 21:22:07 +03:00
issueConfig , err := ctx . IssueConfigFromDefaultBranch ( )
ctx . Data [ "IssueConfig" ] = issueConfig
ctx . Data [ "IssueConfigError" ] = err // ctx.Flash.Err makes problems here
2022-03-22 22:54:24 +03:00
ctx . Data [ "milestone" ] = ctx . FormInt64 ( "milestone" )
ctx . Data [ "project" ] = ctx . FormInt64 ( "project" )
2021-04-05 18:30:52 +03:00
ctx . HTML ( http . StatusOK , tplIssueChoose )
2020-09-11 17:48:39 +03:00
}
2022-03-09 03:38:11 +03:00
// DeleteIssue deletes an issue
func DeleteIssue ( ctx * context . Context ) {
issue := GetActionIssue ( ctx )
if ctx . Written ( ) {
return
}
2023-04-14 21:18:28 +03:00
if err := issue_service . DeleteIssue ( ctx , ctx . Doer , ctx . Repo . GitRepo , issue ) ; err != nil {
2022-03-09 03:38:11 +03:00
ctx . ServerError ( "DeleteIssueByID" , err )
return
}
2022-06-19 13:05:15 +03:00
if issue . IsPull {
2023-02-11 09:34:11 +03:00
ctx . Redirect ( fmt . Sprintf ( "%s/pulls" , ctx . Repo . Repository . Link ( ) ) , http . StatusSeeOther )
2022-06-19 13:05:15 +03:00
return
}
2023-02-11 09:34:11 +03:00
ctx . Redirect ( fmt . Sprintf ( "%s/issues" , ctx . Repo . Repository . Link ( ) ) , http . StatusSeeOther )
2022-03-09 03:38:11 +03:00
}
2021-07-08 14:38:13 +03:00
// ValidateRepoMetas check and returns repository's meta information
2021-04-06 22:44:05 +03:00
func ValidateRepoMetas ( ctx * context . Context , form forms . CreateIssueForm , isPull bool ) ( [ ] int64 , [ ] int64 , int64 , int64 ) {
2015-09-02 02:07:02 +03:00
var (
repo = ctx . Repo . Repository
err error
)
2020-01-19 09:43:38 +03:00
labels := RetrieveRepoMetas ( ctx , ctx . Repo . Repository , isPull )
2015-09-02 02:07:02 +03:00
if ctx . Written ( ) {
2020-08-17 06:07:38 +03:00
return nil , nil , 0 , 0
2015-09-02 02:07:02 +03:00
}
2017-02-01 05:36:08 +03:00
var labelIDs [ ] int64
2015-09-02 02:07:02 +03:00
hasSelected := false
2017-02-01 05:36:08 +03:00
// Check labels.
if len ( form . LabelIDs ) > 0 {
labelIDs , err = base . StringsToInt64s ( strings . Split ( form . LabelIDs , "," ) )
if err != nil {
2020-08-17 06:07:38 +03:00
return nil , nil , 0 , 0
2017-02-01 05:36:08 +03:00
}
2022-10-12 08:18:26 +03:00
labelIDMark := make ( container . Set [ int64 ] )
labelIDMark . AddMultiple ( labelIDs ... )
2017-02-01 05:36:08 +03:00
for i := range labels {
2022-10-12 08:18:26 +03:00
if labelIDMark . Contains ( labels [ i ] . ID ) {
2017-02-01 05:36:08 +03:00
labels [ i ] . IsChecked = true
hasSelected = true
}
2015-09-02 02:07:02 +03:00
}
}
2017-02-01 05:36:08 +03:00
ctx . Data [ "Labels" ] = labels
2015-09-02 02:07:02 +03:00
ctx . Data [ "HasSelectedLabel" ] = hasSelected
ctx . Data [ "label_ids" ] = form . LabelIDs
// Check milestone.
milestoneID := form . MilestoneID
if milestoneID > 0 {
2022-04-08 12:11:15 +03:00
milestone , err := issues_model . GetMilestoneByRepoID ( ctx , ctx . Repo . Repository . ID , milestoneID )
2015-09-02 02:07:02 +03:00
if err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "GetMilestoneByID" , err )
2020-08-17 06:07:38 +03:00
return nil , nil , 0 , 0
2015-09-02 02:07:02 +03:00
}
2021-12-10 04:27:50 +03:00
if milestone . RepoID != repo . ID {
ctx . ServerError ( "GetMilestoneByID" , err )
return nil , nil , 0 , 0
}
ctx . Data [ "Milestone" ] = milestone
2015-09-02 02:07:02 +03:00
ctx . Data [ "milestone_id" ] = milestoneID
}
2020-08-17 06:07:38 +03:00
if form . ProjectID > 0 {
2022-05-20 17:08:52 +03:00
p , err := project_model . GetProjectByID ( ctx , form . ProjectID )
2020-08-17 06:07:38 +03:00
if err != nil {
ctx . ServerError ( "GetProjectByID" , err )
return nil , nil , 0 , 0
}
2023-01-20 14:42:33 +03:00
if p . RepoID != ctx . Repo . Repository . ID && p . OwnerID != ctx . Repo . Repository . OwnerID {
2020-08-17 06:07:38 +03:00
ctx . NotFound ( "" , nil )
return nil , nil , 0 , 0
}
ctx . Data [ "Project" ] = p
ctx . Data [ "project_id" ] = form . ProjectID
}
2018-05-09 19:29:04 +03:00
// Check assignees
var assigneeIDs [ ] int64
if len ( form . AssigneeIDs ) > 0 {
assigneeIDs , err = base . StringsToInt64s ( strings . Split ( form . AssigneeIDs , "," ) )
2015-09-02 02:07:02 +03:00
if err != nil {
2020-08-17 06:07:38 +03:00
return nil , nil , 0 , 0
2018-05-09 19:29:04 +03:00
}
2019-10-25 17:46:37 +03:00
// Check if the passed assignees actually exists and is assignable
2018-05-09 19:29:04 +03:00
for _ , aID := range assigneeIDs {
2022-12-03 05:48:26 +03:00
assignee , err := user_model . GetUserByID ( ctx , aID )
2018-11-28 14:26:14 +03:00
if err != nil {
ctx . ServerError ( "GetUserByID" , err )
2020-08-17 06:07:38 +03:00
return nil , nil , 0 , 0
2018-11-28 14:26:14 +03:00
}
2022-05-11 13:09:36 +03:00
valid , err := access_model . CanBeAssigned ( ctx , assignee , repo , isPull )
2018-05-09 19:29:04 +03:00
if err != nil {
2020-08-17 06:07:38 +03:00
ctx . ServerError ( "CanBeAssigned" , err )
return nil , nil , 0 , 0
2018-11-28 14:26:14 +03:00
}
2020-08-17 06:07:38 +03:00
2019-10-25 17:46:37 +03:00
if ! valid {
2022-06-13 12:37:59 +03:00
ctx . ServerError ( "canBeAssigned" , repo_model . ErrUserDoesNotHaveAccessToRepo { UserID : aID , RepoName : repo . Name } )
2020-08-17 06:07:38 +03:00
return nil , nil , 0 , 0
2018-05-09 19:29:04 +03:00
}
2015-09-02 02:07:02 +03:00
}
}
2018-05-09 19:29:04 +03:00
// Keep the old assignee id thingy for compatibility reasons
if form . AssigneeID > 0 {
assigneeIDs = append ( assigneeIDs , form . AssigneeID )
}
2020-08-17 06:07:38 +03:00
return labelIDs , assigneeIDs , milestoneID , form . ProjectID
2015-09-02 02:07:02 +03:00
}
2016-11-24 10:04:31 +03:00
// NewIssuePost response for creating new issue
2021-01-26 18:36:53 +03:00
func NewIssuePost ( ctx * context . Context ) {
2021-04-06 22:44:05 +03:00
form := web . GetForm ( ctx ) . ( * forms . CreateIssueForm )
2015-08-09 10:23:02 +03:00
ctx . Data [ "Title" ] = ctx . Tr ( "repo.issues.new" )
ctx . Data [ "PageIsIssueList" ] = true
2023-03-28 21:22:07 +03:00
ctx . Data [ "NewIssueChooseTemplate" ] = ctx . HasIssueTemplatesOrContactLinks ( )
2018-08-13 22:04:39 +03:00
ctx . Data [ "PullRequestWorkInProgressPrefixes" ] = setting . Repository . PullRequest . WorkInProgressPrefixes
2020-10-05 08:49:33 +03:00
ctx . Data [ "IsAttachmentEnabled" ] = setting . Attachment . Enabled
upload . AddUploadContext ( ctx , "comment" )
2014-07-26 10:28:04 +04:00
2015-08-10 11:52:08 +03:00
var (
2015-08-10 13:57:57 +03:00
repo = ctx . Repo . Repository
2015-08-11 18:24:40 +03:00
attachments [ ] string
2015-08-10 11:52:08 +03:00
)
2015-08-31 10:24:28 +03:00
2021-01-26 18:36:53 +03:00
labelIDs , assigneeIDs , milestoneID , projectID := ValidateRepoMetas ( ctx , * form , false )
2015-09-02 02:07:02 +03:00
if ctx . Written ( ) {
return
2015-08-10 11:52:08 +03:00
}
2020-08-18 07:23:45 +03:00
if setting . Attachment . Enabled {
2016-08-11 15:48:08 +03:00
attachments = form . Files
2015-08-11 18:24:40 +03:00
}
2014-07-26 10:28:04 +04:00
if ctx . HasError ( ) {
2021-04-05 18:30:52 +03:00
ctx . HTML ( http . StatusOK , tplIssueNew )
2014-07-26 10:28:04 +04:00
return
}
2019-01-21 14:45:32 +03:00
if util . IsEmptyString ( form . Title ) {
ctx . RenderWithErr ( ctx . Tr ( "repo.issues.new.title_empty" ) , tplIssueNew , form )
return
}
2022-09-02 10:58:49 +03:00
content := form . Content
if filename := ctx . Req . Form . Get ( "template-file" ) ; filename != "" {
if template , err := issue_template . UnmarshalFromRepo ( ctx . Repo . GitRepo , ctx . Repo . Repository . DefaultBranch , filename ) ; err == nil {
content = issue_template . RenderToMarkdown ( template , ctx . Req . Form )
}
}
2022-06-13 12:37:59 +03:00
issue := & issues_model . Issue {
2016-03-14 06:20:22 +03:00
RepoID : repo . ID ,
2021-11-16 21:18:25 +03:00
Repo : repo ,
2016-08-14 13:32:24 +03:00
Title : form . Title ,
2022-03-22 10:03:22 +03:00
PosterID : ctx . Doer . ID ,
Poster : ctx . Doer ,
2015-08-10 13:57:57 +03:00
MilestoneID : milestoneID ,
2022-09-02 10:58:49 +03:00
Content : content ,
2017-08-24 15:30:27 +03:00
Ref : form . Ref ,
2014-07-26 10:28:04 +04:00
}
Add Organization Wide Labels (#10814)
* Add organization wide labels
Implement organization wide labels similar to organization wide
webhooks. This lets you create individual labels for organizations that can be used
for all repos under that organization (so being able to reuse the same
label across multiple repos).
This makes it possible for small organizations with many repos to use
labels effectively.
Fixes #7406
* Add migration
* remove comments
* fix tests
* Update options/locale/locale_en-US.ini
Removed unused translation string
* show org labels in issue search label filter
* Use more clear var name
* rename migration after merge from master
* comment typo
* update migration again after rebase with master
* check for orgID <=0 per guillep2k review
* fmt
* Apply suggestions from code review
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* remove unused code
* Make sure RepoID is 0 when searching orgID per code review
* more changes/code review requests
* More descriptive translation var per code review
* func description/delete comment when issue label deleted instead of hiding it
* remove comment
* only use issues in that repo when calculating number of open issues for org label on repo label page
* Add integration test for IssuesSearch API with labels
* remove unused function
* Update models/issue_label.go
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* Use subquery in GetLabelIDsInReposByNames
* Fix tests to use correct orgID
* fix more tests
* IssuesSearch api now uses new BuildLabelNamesIssueIDsCondition. Add a few more tests as well
* update comment for clarity
* Revert previous code change now that we can use the new BuildLabelNamesIssueIDsCondition
* Don't sort repos by date in IssuesSearch API
After much debugging I've found a strange issue where in some cases MySQL will return a different result than other enigines if a query is sorted by a null collumn. For example with our integration test data where we don't set updated_unix in repository fixtures:
SELECT `id`, `owner_id`, `owner_name`, `lower_name`, `name`, `description`, `website`, `original_service_type`, `original_url`, `default_branch`, `num_watches`, `num_stars`, `num_forks`, `num_issues`, `num_closed_issues`, `num_pulls`, `num_closed_pulls`, `num_milestones`, `num_closed_milestones`, `is_private`, `is_empty`, `is_archived`, `is_mirror`, `status`, `is_fork`, `fork_id`, `is_template`, `template_id`, `size`, `is_fsck_enabled`, `close_issues_via_commit_in_any_branch`, `topics`, `avatar`, `created_unix`, `updated_unix` FROM `repository` ORDER BY updated_unix DESC LIMIT 15 OFFSET 45
Returns different results for MySQL than other engines. However, the similar query:
SELECT `id`, `owner_id`, `owner_name`, `lower_name`, `name`, `description`, `website`, `original_service_type`, `original_url`, `default_branch`, `num_watches`, `num_stars`, `num_forks`, `num_issues`, `num_closed_issues`, `num_pulls`, `num_closed_pulls`, `num_milestones`, `num_closed_milestones`, `is_private`, `is_empty`, `is_archived`, `is_mirror`, `status`, `is_fork`, `fork_id`, `is_template`, `template_id`, `size`, `is_fsck_enabled`, `close_issues_via_commit_in_any_branch`, `topics`, `avatar`, `created_unix`, `updated_unix` FROM `repository` ORDER BY updated_unix DESC LIMIT 15 OFFSET 30
Returns the same results.
This causes integration tests to fail on MySQL in certain cases but would never show up in a real installation. Since this API call always returns issues based on the optionally provided repo_priority_id or the issueID itself, there is no change to results by changing the repo sorting method used to get ids earlier in the function.
* linter is back!
* code review
* remove now unused option
* Fix newline at end of files
* more unused code
* update to master
* check for matching ids before query
* Update models/issue_label.go
Co-Authored-By: 6543 <6543@obermui.de>
* Update models/issue_label.go
* update comments
* Update routers/org/setting.go
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
Co-authored-by: 6543 <6543@obermui.de>
2020-04-01 07:14:46 +03:00
2023-04-14 21:18:28 +03:00
if err := issue_service . NewIssue ( ctx , repo , issue , labelIDs , attachments , assigneeIDs ) ; err != nil {
2022-06-13 12:37:59 +03:00
if repo_model . IsErrUserDoesNotHaveAccessToRepo ( err ) {
2021-04-05 18:30:52 +03:00
ctx . Error ( http . StatusBadRequest , "UserDoesNotHaveAccessToRepo" , err . Error ( ) )
2018-05-09 19:29:04 +03:00
return
}
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "NewIssue" , err )
2014-07-26 10:28:04 +04:00
return
2015-08-10 18:31:59 +03:00
}
2020-08-17 06:07:38 +03:00
if projectID > 0 {
2022-06-30 18:55:08 +03:00
if ! ctx . Repo . CanRead ( unit . TypeProjects ) {
// User must also be able to see the project.
ctx . Error ( http . StatusBadRequest , "user hasn't permissions to read projects" )
return
}
2022-06-13 12:37:59 +03:00
if err := issues_model . ChangeProjectAssign ( issue , ctx . Doer , projectID ) ; err != nil {
2020-08-17 06:07:38 +03:00
ctx . ServerError ( "ChangeProjectAssign" , err )
return
}
}
2015-09-02 02:07:02 +03:00
log . Trace ( "Issue created: %d/%d" , repo . ID , issue . ID )
2023-04-08 11:17:50 +03:00
if ctx . FormString ( "redirect_after_creation" ) == "project" && projectID > 0 {
ctx . Redirect ( ctx . Repo . RepoLink + "/projects/" + strconv . FormatInt ( projectID , 10 ) )
2021-10-05 22:21:52 +03:00
} else {
2021-11-16 21:18:25 +03:00
ctx . Redirect ( issue . Link ( ) )
2021-10-05 22:21:52 +03:00
}
2014-07-26 10:28:04 +04:00
}
2022-01-10 12:32:37 +03:00
// roleDescriptor returns the Role Descriptor for a comment in/with the given repo, poster and issue
2023-02-15 20:29:13 +03:00
func roleDescriptor ( ctx stdCtx . Context , repo * repo_model . Repository , poster * user_model . User , issue * issues_model . Issue , hasOriginalAuthor bool ) ( issues_model . RoleDescriptor , error ) {
if hasOriginalAuthor {
return issues_model . RoleDescriptorNone , nil
}
2022-05-11 13:09:36 +03:00
perm , err := access_model . GetUserRepoPermission ( ctx , repo , poster )
2018-11-28 14:26:14 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
return issues_model . RoleDescriptorNone , err
2017-12-21 10:43:26 +03:00
}
2020-11-28 18:52:29 +03:00
2021-11-11 09:29:30 +03:00
// By default the poster has no roles on the comment.
2022-06-13 12:37:59 +03:00
roleDescriptor := issues_model . RoleDescriptorNone
2020-11-28 18:52:29 +03:00
2021-11-11 09:29:30 +03:00
// Check if the poster is owner of the repo.
if perm . IsOwner ( ) {
// If the poster isn't a admin, enable the owner role.
if ! poster . IsAdmin {
2022-06-13 12:37:59 +03:00
roleDescriptor = roleDescriptor . WithRole ( issues_model . RoleDescriptorOwner )
2021-11-11 09:29:30 +03:00
} else {
2020-11-28 18:52:29 +03:00
2021-11-11 09:29:30 +03:00
// Otherwise check if poster is the real repo admin.
2022-05-11 13:09:36 +03:00
ok , err := access_model . IsUserRealRepoAdmin ( repo , poster )
2021-11-11 09:29:30 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
return issues_model . RoleDescriptorNone , err
2021-11-11 09:29:30 +03:00
}
if ok {
2022-06-13 12:37:59 +03:00
roleDescriptor = roleDescriptor . WithRole ( issues_model . RoleDescriptorOwner )
2021-11-11 09:29:30 +03:00
}
2020-11-28 18:52:29 +03:00
}
2021-11-11 09:29:30 +03:00
}
2020-11-28 18:52:29 +03:00
2021-11-11 09:29:30 +03:00
// Is the poster can write issues or pulls to the repo, enable the Writer role.
// Only enable this if the poster doesn't have the owner role already.
if ! roleDescriptor . HasRole ( "Owner" ) && perm . CanWriteIssuesOrPulls ( issue . IsPull ) {
2022-06-13 12:37:59 +03:00
roleDescriptor = roleDescriptor . WithRole ( issues_model . RoleDescriptorWriter )
2020-11-28 18:52:29 +03:00
}
2021-11-11 09:29:30 +03:00
// If the poster is the actual poster of the issue, enable Poster role.
if issue . IsPoster ( poster . ID ) {
2022-06-13 12:37:59 +03:00
roleDescriptor = roleDescriptor . WithRole ( issues_model . RoleDescriptorPoster )
2017-12-21 10:43:26 +03:00
}
2018-11-28 14:26:14 +03:00
2021-11-11 09:29:30 +03:00
return roleDescriptor , nil
2017-12-21 10:43:26 +03:00
}
2022-06-13 12:37:59 +03:00
func getBranchData ( ctx * context . Context , issue * issues_model . Issue ) {
2019-12-16 09:20:25 +03:00
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
2022-11-19 11:12:33 +03:00
ctx . Data [ "HeadUserName" ] = pull . MustHeadUserName ( ctx )
2019-12-16 09:20:25 +03:00
}
}
2016-11-24 10:04:31 +03:00
// ViewIssue render issue view page
2016-03-11 19:56:52 +03:00
func ViewIssue ( ctx * context . Context ) {
2019-12-14 03:53:32 +03:00
if ctx . Params ( ":type" ) == "issues" {
// If issue was requested we check if repo has external tracker and redirect
2022-12-10 05:46:31 +03:00
extIssueUnit , err := ctx . Repo . Repository . GetUnit ( ctx , unit . TypeExternalTracker )
2019-12-14 03:53:32 +03:00
if err == nil && extIssueUnit != nil {
if extIssueUnit . ExternalTrackerConfig ( ) . ExternalTrackerStyle == markup . IssueNameStyleNumeric || extIssueUnit . ExternalTrackerConfig ( ) . ExternalTrackerStyle == "" {
metas := ctx . Repo . Repository . ComposeMetas ( )
metas [ "index" ] = ctx . Params ( ":index" )
2022-04-01 11:47:50 +03:00
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 )
2019-12-14 03:53:32 +03:00
return
}
2021-12-10 04:27:50 +03:00
} else if err != nil && ! repo_model . IsErrUnitTypeNotExist ( err ) {
2019-12-14 03:53:32 +03:00
ctx . ServerError ( "GetUnit" , err )
2019-12-07 07:21:18 +03:00
return
}
}
2022-06-13 12:37:59 +03:00
issue , err := issues_model . GetIssueByIndex ( ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2014-07-26 10:28:04 +04:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrIssueNotExist ( err ) {
2018-01-11 00:34:17 +03:00
ctx . NotFound ( "GetIssueByIndex" , err )
2014-07-26 10:28:04 +04:00
} else {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "GetIssueByIndex" , err )
2014-07-26 10:28:04 +04:00
}
return
}
2021-11-16 21:18:25 +03:00
if issue . Repo == nil {
issue . Repo = ctx . Repo . Repository
}
2017-03-30 02:31:47 +03:00
2015-09-02 02:07:02 +03:00
// Make sure type and URL matches.
if ctx . Params ( ":type" ) == "issues" && issue . IsPull {
2021-11-16 21:18:25 +03:00
ctx . Redirect ( issue . Link ( ) )
2015-09-02 02:07:02 +03:00
return
} else if ctx . Params ( ":type" ) == "pulls" && ! issue . IsPull {
2021-11-16 21:18:25 +03:00
ctx . Redirect ( issue . Link ( ) )
2015-09-02 02:07:02 +03:00
return
}
2015-09-02 11:08:05 +03:00
if issue . IsPull {
2016-03-07 07:57:46 +03:00
MustAllowPulls ( ctx )
if ctx . Written ( ) {
return
}
ctx . Data [ "PageIsPullList" ] = true
2015-09-02 11:08:05 +03:00
ctx . Data [ "PageIsPullConversation" ] = true
} else {
2015-12-05 05:30:33 +03:00
MustEnableIssues ( ctx )
if ctx . Written ( ) {
return
}
2015-09-02 11:08:05 +03:00
ctx . Data [ "PageIsIssueList" ] = true
2023-03-28 21:22:07 +03:00
ctx . Data [ "NewIssueChooseTemplate" ] = ctx . HasIssueTemplatesOrContactLinks ( )
2015-09-02 11:08:05 +03:00
}
2021-11-09 22:57:58 +03:00
if issue . IsPull && ! ctx . Repo . CanRead ( unit . TypeIssues ) {
2020-01-19 09:43:38 +03:00
ctx . Data [ "IssueType" ] = "pulls"
2021-11-09 22:57:58 +03:00
} else if ! issue . IsPull && ! ctx . Repo . CanRead ( unit . TypePullRequests ) {
2020-01-19 09:43:38 +03:00
ctx . Data [ "IssueType" ] = "issues"
} else {
ctx . Data [ "IssueType" ] = "all"
}
2021-11-09 22:57:58 +03:00
ctx . Data [ "IsProjectsEnabled" ] = ctx . Repo . CanRead ( unit . TypeProjects )
2020-10-05 08:49:33 +03:00
ctx . Data [ "IsAttachmentEnabled" ] = setting . Attachment . Enabled
upload . AddUploadContext ( ctx , "comment" )
2018-11-28 14:26:14 +03:00
2022-06-13 12:37:59 +03:00
if err = issue . LoadAttributes ( ctx ) ; err != nil {
2019-09-20 08:45:38 +03:00
ctx . ServerError ( "LoadAttributes" , err )
return
}
if err = filterXRefComments ( ctx , issue ) ; err != nil {
ctx . ServerError ( "filterXRefComments" , err )
2018-12-13 18:55:43 +03:00
return
}
2018-11-28 14:26:14 +03:00
ctx . Data [ "Title" ] = fmt . Sprintf ( "#%d - %s" , issue . Index , issue . Title )
2022-06-13 12:37:59 +03:00
iw := new ( issues_model . IssueWatch )
2022-03-22 10:03:22 +03:00
if ctx . Doer != nil {
iw . UserID = ctx . Doer . ID
2020-04-21 16:48:53 +03:00
iw . IssueID = issue . ID
2022-06-13 12:37:59 +03:00
iw . IsWatching , err = issues_model . CheckIssueWatch ( ctx . Doer , issue )
2018-11-28 14:26:14 +03:00
if err != nil {
2021-01-14 23:27:22 +03:00
ctx . ServerError ( "CheckIssueWatch" , err )
2018-11-28 14:26:14 +03:00
return
}
}
ctx . Data [ "IssueWatch" ] = iw
2021-04-20 01:25:08 +03:00
issue . RenderedContent , err = markdown . RenderString ( & markup . RenderContext {
URLPrefix : ctx . Repo . RepoLink ,
Metas : ctx . Repo . Repository . ComposeMetas ( ) ,
2021-06-21 01:39:12 +03:00
GitRepo : ctx . Repo . GitRepo ,
2021-08-28 23:15:56 +03:00
Ctx : ctx ,
2021-04-20 01:25:08 +03:00
} , issue . Content )
if err != nil {
ctx . ServerError ( "RenderString" , err )
return
}
2014-07-26 10:28:04 +04:00
2015-08-14 19:42:43 +03:00
repo := ctx . Repo . Repository
2015-09-02 02:07:02 +03:00
// Get more information if it's a pull request.
if issue . IsPull {
2016-08-16 20:19:09 +03:00
if issue . PullRequest . HasMerged {
ctx . Data [ "DisableStatusChange" ] = issue . PullRequest . HasMerged
2015-09-02 16:26:56 +03:00
PrepareMergedViewPullInfo ( ctx , issue )
} else {
PrepareViewPullInfo ( ctx , issue )
2019-04-20 23:50:34 +03:00
ctx . Data [ "DisableStatusChange" ] = ctx . Data [ "IsPullRequestBroken" ] == true && issue . IsClosed
2015-09-02 16:26:56 +03:00
}
2015-09-02 11:08:05 +03:00
if ctx . Written ( ) {
2015-09-02 02:07:02 +03:00
return
}
}
2015-08-12 12:04:23 +03:00
// Metas.
2015-08-14 19:42:43 +03:00
// Check labels.
2022-10-12 08:18:26 +03:00
labelIDMark := make ( container . Set [ int64 ] )
for _ , label := range issue . Labels {
labelIDMark . Add ( label . ID )
2015-08-14 19:42:43 +03:00
}
2022-06-13 12:37:59 +03:00
labels , err := issues_model . GetLabelsByRepoID ( ctx , repo . ID , "" , db . ListOptions { } )
2015-08-14 19:42:43 +03:00
if err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "GetLabelsByRepoID" , err )
2015-08-14 19:42:43 +03:00
return
}
Add Organization Wide Labels (#10814)
* Add organization wide labels
Implement organization wide labels similar to organization wide
webhooks. This lets you create individual labels for organizations that can be used
for all repos under that organization (so being able to reuse the same
label across multiple repos).
This makes it possible for small organizations with many repos to use
labels effectively.
Fixes #7406
* Add migration
* remove comments
* fix tests
* Update options/locale/locale_en-US.ini
Removed unused translation string
* show org labels in issue search label filter
* Use more clear var name
* rename migration after merge from master
* comment typo
* update migration again after rebase with master
* check for orgID <=0 per guillep2k review
* fmt
* Apply suggestions from code review
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* remove unused code
* Make sure RepoID is 0 when searching orgID per code review
* more changes/code review requests
* More descriptive translation var per code review
* func description/delete comment when issue label deleted instead of hiding it
* remove comment
* only use issues in that repo when calculating number of open issues for org label on repo label page
* Add integration test for IssuesSearch API with labels
* remove unused function
* Update models/issue_label.go
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* Use subquery in GetLabelIDsInReposByNames
* Fix tests to use correct orgID
* fix more tests
* IssuesSearch api now uses new BuildLabelNamesIssueIDsCondition. Add a few more tests as well
* update comment for clarity
* Revert previous code change now that we can use the new BuildLabelNamesIssueIDsCondition
* Don't sort repos by date in IssuesSearch API
After much debugging I've found a strange issue where in some cases MySQL will return a different result than other enigines if a query is sorted by a null collumn. For example with our integration test data where we don't set updated_unix in repository fixtures:
SELECT `id`, `owner_id`, `owner_name`, `lower_name`, `name`, `description`, `website`, `original_service_type`, `original_url`, `default_branch`, `num_watches`, `num_stars`, `num_forks`, `num_issues`, `num_closed_issues`, `num_pulls`, `num_closed_pulls`, `num_milestones`, `num_closed_milestones`, `is_private`, `is_empty`, `is_archived`, `is_mirror`, `status`, `is_fork`, `fork_id`, `is_template`, `template_id`, `size`, `is_fsck_enabled`, `close_issues_via_commit_in_any_branch`, `topics`, `avatar`, `created_unix`, `updated_unix` FROM `repository` ORDER BY updated_unix DESC LIMIT 15 OFFSET 45
Returns different results for MySQL than other engines. However, the similar query:
SELECT `id`, `owner_id`, `owner_name`, `lower_name`, `name`, `description`, `website`, `original_service_type`, `original_url`, `default_branch`, `num_watches`, `num_stars`, `num_forks`, `num_issues`, `num_closed_issues`, `num_pulls`, `num_closed_pulls`, `num_milestones`, `num_closed_milestones`, `is_private`, `is_empty`, `is_archived`, `is_mirror`, `status`, `is_fork`, `fork_id`, `is_template`, `template_id`, `size`, `is_fsck_enabled`, `close_issues_via_commit_in_any_branch`, `topics`, `avatar`, `created_unix`, `updated_unix` FROM `repository` ORDER BY updated_unix DESC LIMIT 15 OFFSET 30
Returns the same results.
This causes integration tests to fail on MySQL in certain cases but would never show up in a real installation. Since this API call always returns issues based on the optionally provided repo_priority_id or the issueID itself, there is no change to results by changing the repo sorting method used to get ids earlier in the function.
* linter is back!
* code review
* remove now unused option
* Fix newline at end of files
* more unused code
* update to master
* check for matching ids before query
* Update models/issue_label.go
Co-Authored-By: 6543 <6543@obermui.de>
* Update models/issue_label.go
* update comments
* Update routers/org/setting.go
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
Co-authored-by: 6543 <6543@obermui.de>
2020-04-01 07:14:46 +03:00
ctx . Data [ "Labels" ] = labels
if repo . Owner . IsOrganization ( ) {
2022-06-13 12:37:59 +03:00
orgLabels , err := issues_model . GetLabelsByOrgID ( ctx , repo . Owner . ID , ctx . FormString ( "sort" ) , db . ListOptions { } )
Add Organization Wide Labels (#10814)
* Add organization wide labels
Implement organization wide labels similar to organization wide
webhooks. This lets you create individual labels for organizations that can be used
for all repos under that organization (so being able to reuse the same
label across multiple repos).
This makes it possible for small organizations with many repos to use
labels effectively.
Fixes #7406
* Add migration
* remove comments
* fix tests
* Update options/locale/locale_en-US.ini
Removed unused translation string
* show org labels in issue search label filter
* Use more clear var name
* rename migration after merge from master
* comment typo
* update migration again after rebase with master
* check for orgID <=0 per guillep2k review
* fmt
* Apply suggestions from code review
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* remove unused code
* Make sure RepoID is 0 when searching orgID per code review
* more changes/code review requests
* More descriptive translation var per code review
* func description/delete comment when issue label deleted instead of hiding it
* remove comment
* only use issues in that repo when calculating number of open issues for org label on repo label page
* Add integration test for IssuesSearch API with labels
* remove unused function
* Update models/issue_label.go
Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>
* Use subquery in GetLabelIDsInReposByNames
* Fix tests to use correct orgID
* fix more tests
* IssuesSearch api now uses new BuildLabelNamesIssueIDsCondition. Add a few more tests as well
* update comment for clarity
* Revert previous code change now that we can use the new BuildLabelNamesIssueIDsCondition
* Don't sort repos by date in IssuesSearch API
After much debugging I've found a strange issue where in some cases MySQL will return a different result than other enigines if a query is sorted by a null collumn. For example with our integration test data where we don't set updated_unix in repository fixtures:
SELECT `id`, `owner_id`, `owner_name`, `lower_name`, `name`, `description`, `website`, `original_service_type`, `original_url`, `default_branch`, `num_watches`, `num_stars`, `num_forks`, `num_issues`, `num_closed_issues`, `num_pulls`, `num_closed_pulls`, `num_milestones`, `num_closed_milestones`, `is_private`, `is_empty`, `is_archived`, `is_mirror`, `status`, `is_fork`, `fork_id`, `is_template`, `template_id`, `size`, `is_fsck_enabled`, `close_issues_via_commit_in_any_branch`, `topics`, `avatar`, `created_unix`, `updated_unix` FROM `repository` ORDER BY updated_unix DESC LIMIT 15 OFFSET 45
Returns different results for MySQL than other engines. However, the similar query:
SELECT `id`, `owner_id`, `owner_name`, `lower_name`, `name`, `description`, `website`, `original_service_type`, `original_url`, `default_branch`, `num_watches`, `num_stars`, `num_forks`, `num_issues`, `num_closed_issues`, `num_pulls`, `num_closed_pulls`, `num_milestones`, `num_closed_milestones`, `is_private`, `is_empty`, `is_archived`, `is_mirror`, `status`, `is_fork`, `fork_id`, `is_template`, `template_id`, `size`, `is_fsck_enabled`, `close_issues_via_commit_in_any_branch`, `topics`, `avatar`, `created_unix`, `updated_unix` FROM `repository` ORDER BY updated_unix DESC LIMIT 15 OFFSET 30
Returns the same results.
This causes integration tests to fail on MySQL in certain cases but would never show up in a real installation. Since this API call always returns issues based on the optionally provided repo_priority_id or the issueID itself, there is no change to results by changing the repo sorting method used to get ids earlier in the function.
* linter is back!
* code review
* remove now unused option
* Fix newline at end of files
* more unused code
* update to master
* check for matching ids before query
* Update models/issue_label.go
Co-Authored-By: 6543 <6543@obermui.de>
* Update models/issue_label.go
* update comments
* Update routers/org/setting.go
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
Co-authored-by: 6543 <6543@obermui.de>
2020-04-01 07:14:46 +03:00
if err != nil {
ctx . ServerError ( "GetLabelsByOrgID" , err )
return
}
ctx . Data [ "OrgLabels" ] = orgLabels
labels = append ( labels , orgLabels ... )
}
2015-08-14 19:42:43 +03:00
hasSelected := false
for i := range labels {
2022-10-12 08:18:26 +03:00
if labelIDMark . Contains ( labels [ i ] . ID ) {
2015-08-14 19:42:43 +03:00
labels [ i ] . IsChecked = true
hasSelected = true
}
}
ctx . Data [ "HasSelectedLabel" ] = hasSelected
// Check milestone and assignee.
2018-11-28 14:26:14 +03:00
if ctx . Repo . CanWriteIssuesOrPulls ( issue . IsPull ) {
2015-09-02 02:07:02 +03:00
RetrieveRepoMilestonesAndAssignees ( ctx , repo )
2020-08-17 06:07:38 +03:00
retrieveProjects ( ctx , repo )
2015-09-02 02:07:02 +03:00
if ctx . Written ( ) {
2015-08-14 19:42:43 +03:00
return
}
}
2014-07-26 10:28:04 +04:00
2020-04-06 19:33:34 +03:00
if issue . IsPull {
2021-11-09 22:57:58 +03:00
canChooseReviewer := ctx . Repo . CanWrite ( unit . TypePullRequests )
2022-09-09 20:27:47 +03:00
if ctx . Doer != nil && ctx . IsSigned {
if ! canChooseReviewer {
canChooseReviewer = ctx . Doer . ID == issue . PosterID
}
if ! canChooseReviewer {
canChooseReviewer , err = issues_model . IsOfficialReviewer ( ctx , issue , ctx . Doer )
if err != nil {
ctx . ServerError ( "IsOfficialReviewer" , err )
return
}
2020-04-06 19:33:34 +03:00
}
}
2020-10-12 22:55:13 +03:00
RetrieveRepoReviewers ( ctx , repo , issue , canChooseReviewer )
2020-04-06 19:33:34 +03:00
if ctx . Written ( ) {
return
}
}
2015-08-12 12:04:23 +03:00
if ctx . IsSigned {
2015-08-12 13:44:09 +03:00
// Update issue-user.
2022-08-25 05:31:57 +03:00
if err = activities_model . SetIssueReadBy ( ctx , issue . ID , ctx . Doer . ID ) ; err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "ReadBy" , err )
2015-08-12 13:44:09 +03:00
return
}
2015-08-12 12:04:23 +03:00
}
2015-08-13 21:43:40 +03:00
var (
Fix cannot reopen after pushing commits to a closed PR (#23189)
Close: #22784
1. On GH, we can reopen a PR which was closed before after pushing
commits. After reopening PR, we can see the commits that were pushed
after closing PR in the time line. So the case of
[issue](https://github.com/go-gitea/gitea/issues/22784) is a bug which
needs to be fixed.
2. After closing a PR and pushing commits, `headBranchSha` is not equal
to `sha`(which is the last commit ID string of reference). If the
judgement exists, the button of reopen will not display. So, skip the
judgement if the status of PR is closed.
![image](https://user-images.githubusercontent.com/33891828/222037529-651fccf9-0bba-433e-b2f0-79c17e0cc812.png)
3. Even if PR is already close, we should still insert comment record
into DB when we push commits.
So we should still call function `CreatePushPullComment()`.
https://github.com/go-gitea/gitea/blob/067b0c2664d127c552ccdfd264257caca4907a77/services/pull/pull.go#L260-L282
So, I add a switch(`includeClosed`) to the
`GetUnmergedPullRequestsByHeadInfo` func to control whether the status
of PR must be open. In this case, by setting `includeClosed` to `true`,
we can query the closed PR.
![image](https://user-images.githubusercontent.com/33891828/222621045-bb80987c-10c5-4eac-aa0c-1fb9c6aefb51.png)
4. In the loop of comments, I use the`latestCloseCommentID` variable to
record the last occurrence of the close comment.
In the go template, if the status of PR is closed, the comments whose
type is `CommentTypePullRequestPush(29)` after `latestCloseCommentID`
won't be rendered.
![image](https://user-images.githubusercontent.com/33891828/222058913-c91cf3e3-819b-40c5-8015-654b31eeccff.png)
e.g.
1). The initial status of the PR is opened.
![image](https://user-images.githubusercontent.com/33891828/222453617-33c5093e-f712-4cd6-8489-9f87e2075869.png)
2). Then I click the button of `Close`. PR is closed now.
![image](https://user-images.githubusercontent.com/33891828/222453694-25c588a9-c121-4897-9ae5-0b13cf33d20b.png)
3). I try to push a commit to this PR, even though its current status is
closed.
![image](https://user-images.githubusercontent.com/33891828/222453916-361678fb-7321-410d-9e37-5a26e8095638.png)
But in comments list, this commit do not display.This is as expected :)
![image](https://user-images.githubusercontent.com/33891828/222454169-7617a791-78d2-404e-be5e-77d555f93313.png)
4). Click the `Reopen` button, the commit which is pushed after closing
PR display now.
![image](https://user-images.githubusercontent.com/33891828/222454533-897893b6-b96e-4701-b5cb-b1800f382b8f.png)
---------
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-03-03 16:16:58 +03:00
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
2015-08-13 21:43:40 +03:00
)
2022-12-10 05:46:31 +03:00
if ctx . Repo . Repository . IsTimetrackerEnabled ( ctx ) {
2017-09-12 09:48:13 +03:00
if ctx . IsSigned {
// Deal with the stopwatch
2022-06-13 12:37:59 +03:00
ctx . Data [ "IsStopwatchRunning" ] = issues_model . StopwatchExists ( ctx . Doer . ID , issue . ID )
2017-09-12 09:48:13 +03:00
if ! ctx . Data [ "IsStopwatchRunning" ] . ( bool ) {
var exists bool
2023-02-27 21:46:00 +03:00
var swIssue * issues_model . Issue
if exists , _ , swIssue , err = issues_model . HasUserStopwatch ( ctx , ctx . Doer . ID ) ; err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "HasUserStopwatch" , err )
2017-09-12 09:48:13 +03:00
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
2023-02-27 21:46:00 +03:00
ctx . Data [ "OtherStopwatchURL" ] = swIssue . Link ( )
2017-09-12 09:48:13 +03:00
}
}
2022-03-22 10:03:22 +03:00
ctx . Data [ "CanUseTimetracker" ] = ctx . Repo . CanUseTimetracker ( issue , ctx . Doer )
2017-09-12 09:48:13 +03:00
} else {
ctx . Data [ "CanUseTimetracker" ] = false
}
2022-06-13 12:37:59 +03:00
if ctx . Data [ "WorkingUsers" ] , err = issues_model . TotalTimes ( & issues_model . FindTrackedTimesOptions { IssueID : issue . ID } ) ; err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "TotalTimes" , err )
2017-09-12 09:48:13 +03:00
return
}
}
2016-02-02 04:55:12 +03:00
2018-07-18 00:23:58 +03:00
// Check if the user can use the dependencies
2022-03-22 10:03:22 +03:00
ctx . Data [ "CanCreateIssueDependencies" ] = ctx . Repo . CanCreateIssueDependencies ( ctx . Doer , issue . IsPull )
2018-07-18 00:23:58 +03:00
Allow cross-repository dependencies on issues (#7901)
* in progress changes for #7405, added ability to add cross-repo dependencies
* removed unused repolink var
* fixed query that was breaking ci tests; fixed check in issue dependency add so that the id of the issue and dependency is checked rather than the indexes
* reverted removal of string in local files becasue these are done via crowdin, not updated manually
* removed 'Select("issue.*")' from getBlockedByDependencies and getBlockingDependencies based on comments in PR review
* changed getBlockedByDependencies and getBlockingDependencies to use a more xorm-like query, also updated the sidebar as a result
* simplified the getBlockingDependencies and getBlockedByDependencies methods; changed the sidebar to show the dependencies in a different format where you can see the name of the repository
* made some changes to the issue view in the dependencies (issue name on top, repo full name on separate line). Change view of issue in the dependency search results (also showing the full repo name on separate line)
* replace call to FindUserAccessibleRepoIDs with SearchRepositoryByName. The former was hardcoded to use isPrivate = false on the repo search, but this code needed it to be true. The SearchRepositoryByName method is used more in the code including on the user's dashboard
* some more tweaks to the layout of the issues when showing dependencies and in the search box when you add new dependencies
* added Name to the RepositoryMeta struct
* updated swagger doc
* fixed total count for link header on SearchIssues
* fixed indentation
* fixed aligment of remove icon on dependencies in issue sidebar
* removed unnecessary nil check (unnecessary because issue.loadRepo is called prior to this block)
* reverting .css change, somehow missed or forgot that less is used
* updated less file and generated css; updated sidebar template with styles to line up delete and issue index
* added ordering to the blocked by/depends on queries
* fixed sorting in issue dependency search and the depends on/blocks views to show issues from the current repo first, then by created date descending; added a "all cross repository dependencies" setting to allow this feature to be turned off, if turned off, the issue dependency search will work the way it did before (restricted to the current repository)
* re-applied my swagger changes after merge
* fixed split string condition in issue search
* changed ALLOW_CROSS_REPOSITORY_DEPENDENCIES description to sound more global than just the issue dependency search; returning 400 in the cross repo issue search api method if not enabled; fixed bug where the issue count did not respect the state parameter
* when adding a dependency to an issue, added a check to make sure the issue and dependency are in the same repo if cross repo dependencies is not enabled
* updated sortIssuesSession call in PullRequests, another commit moved this method from pull.go to pull_list.go so I had to re-apply my change here
* fixed incorrect setting of user id parameter in search repos call
2019-10-31 08:06:10 +03:00
// check if dependencies can be created across repositories
ctx . Data [ "AllowCrossRepositoryDependencies" ] = setting . Service . AllowCrossRepositoryDependencies
2023-02-15 20:29:13 +03:00
if issue . ShowRole , err = roleDescriptor ( ctx , repo , issue . Poster , issue , issue . HasOriginalAuthor ( ) ) ; err != nil {
2021-11-11 09:29:30 +03:00
ctx . ServerError ( "roleDescriptor" , err )
2020-09-10 21:09:14 +03:00
return
}
2021-11-11 09:29:30 +03:00
marked [ issue . PosterID ] = issue . ShowRole
2020-09-10 21:09:14 +03:00
2016-02-02 04:55:12 +03:00
// Render comments and and fetch participants.
participants [ 0 ] = issue . Poster
2015-08-13 21:43:40 +03:00
for _ , comment = range issue . Comments {
2019-05-06 15:09:31 +03:00
comment . Issue = issue
2022-11-19 11:12:33 +03:00
if err := comment . LoadPoster ( ctx ) ; err != nil {
2018-12-13 18:55:43 +03:00
ctx . ServerError ( "LoadPoster" , err )
return
}
2022-06-13 12:37:59 +03:00
if comment . Type == issues_model . CommentTypeComment || comment . Type == issues_model . CommentTypeReview {
2022-11-19 11:12:33 +03:00
if err := comment . LoadAttachments ( ctx ) ; err != nil {
2018-12-13 18:55:43 +03:00
ctx . ServerError ( "LoadAttachments" , err )
return
}
2021-04-20 01:25:08 +03:00
comment . RenderedContent , err = markdown . RenderString ( & markup . RenderContext {
URLPrefix : ctx . Repo . RepoLink ,
Metas : ctx . Repo . Repository . ComposeMetas ( ) ,
2021-06-21 01:39:12 +03:00
GitRepo : ctx . Repo . GitRepo ,
2021-08-28 23:15:56 +03:00
Ctx : ctx ,
2021-04-20 01:25:08 +03:00
} , comment . Content )
if err != nil {
ctx . ServerError ( "RenderString" , err )
return
}
2015-08-13 21:43:40 +03:00
// Check tag.
2021-11-11 09:29:30 +03:00
role , ok = marked [ comment . PosterID ]
2015-08-13 21:43:40 +03:00
if ok {
2021-11-11 09:29:30 +03:00
comment . ShowRole = role
2015-08-13 21:43:40 +03:00
continue
}
2023-02-15 20:29:13 +03:00
comment . ShowRole , err = roleDescriptor ( ctx , repo , comment . Poster , issue , comment . HasOriginalAuthor ( ) )
2017-12-21 10:43:26 +03:00
if err != nil {
2021-11-11 09:29:30 +03:00
ctx . ServerError ( "roleDescriptor" , err )
2017-12-21 10:43:26 +03:00
return
2015-08-13 21:43:40 +03:00
}
2021-11-11 09:29:30 +03:00
marked [ comment . PosterID ] = comment . ShowRole
2019-09-07 17:53:35 +03:00
participants = addParticipant ( comment . Poster , participants )
2022-06-13 12:37:59 +03:00
} else if comment . Type == issues_model . CommentTypeLabel {
2017-01-30 15:46:45 +03:00
if err = comment . LoadLabel ( ) ; err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "LoadLabel" , err )
2017-01-30 15:46:45 +03:00
return
}
2022-06-13 12:37:59 +03:00
} else if comment . Type == issues_model . CommentTypeMilestone {
2022-11-19 11:12:33 +03:00
if err = comment . LoadMilestone ( ctx ) ; err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "LoadMilestone" , err )
2017-02-01 05:36:08 +03:00
return
}
2022-04-08 12:11:15 +03:00
ghostMilestone := & issues_model . Milestone {
2017-06-17 07:51:28 +03:00
ID : - 1 ,
Name : ctx . Tr ( "repo.issues.deleted_milestone" ) ,
}
if comment . OldMilestoneID > 0 && comment . OldMilestone == nil {
comment . OldMilestone = ghostMilestone
}
if comment . MilestoneID > 0 && comment . Milestone == nil {
comment . Milestone = ghostMilestone
}
2022-06-13 12:37:59 +03:00
} else if comment . Type == issues_model . CommentTypeProject {
2020-08-17 06:07:38 +03:00
if err = comment . LoadProject ( ) ; err != nil {
ctx . ServerError ( "LoadProject" , err )
return
}
2022-03-29 17:16:31 +03:00
ghostProject := & project_model . Project {
2020-08-17 06:07:38 +03:00
ID : - 1 ,
Title : ctx . Tr ( "repo.issues.deleted_project" ) ,
}
if comment . OldProjectID > 0 && comment . OldProject == nil {
comment . OldProject = ghostProject
}
if comment . ProjectID > 0 && comment . Project == nil {
comment . Project = ghostProject
}
2022-06-13 12:37:59 +03:00
} else if comment . Type == issues_model . CommentTypeAssignees || comment . Type == issues_model . CommentTypeReviewRequest {
2020-10-12 22:55:13 +03:00
if err = comment . LoadAssigneeUserAndTeam ( ) ; err != nil {
ctx . ServerError ( "LoadAssigneeUserAndTeam" , err )
2017-02-03 18:09:10 +03:00
return
}
2022-06-13 12:37:59 +03:00
} else if comment . Type == issues_model . CommentTypeRemoveDependency || comment . Type == issues_model . CommentTypeAddDependency {
2018-07-18 00:23:58 +03:00
if err = comment . LoadDepIssueDetails ( ) ; err != nil {
2022-06-13 12:37:59 +03:00
if ! issues_model . IsErrIssueNotExist ( err ) {
2020-09-04 04:36:56 +03:00
ctx . ServerError ( "LoadDepIssueDetails" , err )
return
}
2018-07-18 00:23:58 +03:00
}
2023-04-20 09:39:44 +03:00
} else if comment . Type . HasContentSupport ( ) {
2021-04-20 01:25:08 +03:00
comment . RenderedContent , err = markdown . RenderString ( & markup . RenderContext {
URLPrefix : ctx . Repo . RepoLink ,
Metas : ctx . Repo . Repository . ComposeMetas ( ) ,
2021-06-21 01:39:12 +03:00
GitRepo : ctx . Repo . GitRepo ,
2021-08-28 23:15:56 +03:00
Ctx : ctx ,
2021-04-20 01:25:08 +03:00
} , comment . Content )
if err != nil {
ctx . ServerError ( "RenderString" , err )
return
}
2022-06-13 12:37:59 +03:00
if err = comment . LoadReview ( ) ; err != nil && ! issues_model . IsErrReviewNotExist ( err ) {
2018-08-06 07:43:22 +03:00
ctx . ServerError ( "LoadReview" , err )
return
}
2019-09-07 17:53:35 +03:00
participants = addParticipant ( comment . Poster , participants )
2018-08-06 07:43:22 +03:00
if comment . Review == nil {
continue
}
2022-01-20 02:26:57 +03:00
if err = comment . Review . LoadAttributes ( ctx ) ; err != nil {
2021-11-24 12:49:20 +03:00
if ! user_model . IsErrUserNotExist ( err ) {
2019-05-06 15:09:31 +03:00
ctx . ServerError ( "Review.LoadAttributes" , err )
return
}
2021-11-24 12:49:20 +03:00
comment . Review . Reviewer = user_model . NewGhostUser ( )
2018-08-06 07:43:22 +03:00
}
2022-01-20 02:26:57 +03:00
if err = comment . Review . LoadCodeComments ( ctx ) ; err != nil {
2018-08-06 07:43:22 +03:00
ctx . ServerError ( "Review.LoadCodeComments" , err )
return
}
2021-01-17 20:29:10 +03:00
for _ , codeComments := range comment . Review . CodeComments {
for _ , lineComments := range codeComments {
for _ , c := range lineComments {
// Check tag.
2021-11-11 09:29:30 +03:00
role , ok = marked [ c . PosterID ]
2021-01-17 20:29:10 +03:00
if ok {
2021-11-11 09:29:30 +03:00
c . ShowRole = role
2021-01-17 20:29:10 +03:00
continue
}
2020-04-18 16:50:25 +03:00
2023-02-15 20:29:13 +03:00
c . ShowRole , err = roleDescriptor ( ctx , repo , c . Poster , issue , c . HasOriginalAuthor ( ) )
2021-01-17 20:29:10 +03:00
if err != nil {
2021-11-11 09:29:30 +03:00
ctx . ServerError ( "roleDescriptor" , err )
2021-01-17 20:29:10 +03:00
return
}
2021-11-11 09:29:30 +03:00
marked [ c . PosterID ] = c . ShowRole
2021-01-17 20:29:10 +03:00
participants = addParticipant ( c . Poster , participants )
}
}
}
2020-04-18 16:50:25 +03:00
if err = comment . LoadResolveDoer ( ) ; err != nil {
ctx . ServerError ( "LoadResolveDoer" , err )
return
}
2022-06-13 12:37:59 +03:00
} else if comment . Type == issues_model . CommentTypePullRequestPush {
2020-05-20 15:47:24 +03:00
participants = addParticipant ( comment . Poster , participants )
2022-01-20 02:26:57 +03:00
if err = comment . LoadPushCommits ( ctx ) ; err != nil {
2020-05-20 15:47:24 +03:00
ctx . ServerError ( "LoadPushCommits" , err )
return
}
2022-06-13 12:37:59 +03:00
} else if comment . Type == issues_model . CommentTypeAddTimeManual ||
comment . Type == issues_model . CommentTypeStopTracking {
2021-02-19 13:52:11 +03:00
// drop error since times could be pruned from DB..
_ = comment . LoadTime ( )
2023-04-21 19:36:37 +03:00
}
if comment . Type == issues_model . CommentTypeClose || comment . Type == issues_model . CommentTypeMergePull {
// record ID of the latest closed/merged comment.
Fix cannot reopen after pushing commits to a closed PR (#23189)
Close: #22784
1. On GH, we can reopen a PR which was closed before after pushing
commits. After reopening PR, we can see the commits that were pushed
after closing PR in the time line. So the case of
[issue](https://github.com/go-gitea/gitea/issues/22784) is a bug which
needs to be fixed.
2. After closing a PR and pushing commits, `headBranchSha` is not equal
to `sha`(which is the last commit ID string of reference). If the
judgement exists, the button of reopen will not display. So, skip the
judgement if the status of PR is closed.
![image](https://user-images.githubusercontent.com/33891828/222037529-651fccf9-0bba-433e-b2f0-79c17e0cc812.png)
3. Even if PR is already close, we should still insert comment record
into DB when we push commits.
So we should still call function `CreatePushPullComment()`.
https://github.com/go-gitea/gitea/blob/067b0c2664d127c552ccdfd264257caca4907a77/services/pull/pull.go#L260-L282
So, I add a switch(`includeClosed`) to the
`GetUnmergedPullRequestsByHeadInfo` func to control whether the status
of PR must be open. In this case, by setting `includeClosed` to `true`,
we can query the closed PR.
![image](https://user-images.githubusercontent.com/33891828/222621045-bb80987c-10c5-4eac-aa0c-1fb9c6aefb51.png)
4. In the loop of comments, I use the`latestCloseCommentID` variable to
record the last occurrence of the close comment.
In the go template, if the status of PR is closed, the comments whose
type is `CommentTypePullRequestPush(29)` after `latestCloseCommentID`
won't be rendered.
![image](https://user-images.githubusercontent.com/33891828/222058913-c91cf3e3-819b-40c5-8015-654b31eeccff.png)
e.g.
1). The initial status of the PR is opened.
![image](https://user-images.githubusercontent.com/33891828/222453617-33c5093e-f712-4cd6-8489-9f87e2075869.png)
2). Then I click the button of `Close`. PR is closed now.
![image](https://user-images.githubusercontent.com/33891828/222453694-25c588a9-c121-4897-9ae5-0b13cf33d20b.png)
3). I try to push a commit to this PR, even though its current status is
closed.
![image](https://user-images.githubusercontent.com/33891828/222453916-361678fb-7321-410d-9e37-5a26e8095638.png)
But in comments list, this commit do not display.This is as expected :)
![image](https://user-images.githubusercontent.com/33891828/222454169-7617a791-78d2-404e-be5e-77d555f93313.png)
4). Click the `Reopen` button, the commit which is pushed after closing
PR display now.
![image](https://user-images.githubusercontent.com/33891828/222454533-897893b6-b96e-4701-b5cb-b1800f382b8f.png)
---------
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-03-03 16:16:58 +03:00
// if PR is closed, the comments whose type is CommentTypePullRequestPush(29) after latestCloseCommentID won't be rendered.
latestCloseCommentID = comment . ID
2015-08-13 11:07:11 +03:00
}
}
2014-07-26 10:28:04 +04:00
Fix cannot reopen after pushing commits to a closed PR (#23189)
Close: #22784
1. On GH, we can reopen a PR which was closed before after pushing
commits. After reopening PR, we can see the commits that were pushed
after closing PR in the time line. So the case of
[issue](https://github.com/go-gitea/gitea/issues/22784) is a bug which
needs to be fixed.
2. After closing a PR and pushing commits, `headBranchSha` is not equal
to `sha`(which is the last commit ID string of reference). If the
judgement exists, the button of reopen will not display. So, skip the
judgement if the status of PR is closed.
![image](https://user-images.githubusercontent.com/33891828/222037529-651fccf9-0bba-433e-b2f0-79c17e0cc812.png)
3. Even if PR is already close, we should still insert comment record
into DB when we push commits.
So we should still call function `CreatePushPullComment()`.
https://github.com/go-gitea/gitea/blob/067b0c2664d127c552ccdfd264257caca4907a77/services/pull/pull.go#L260-L282
So, I add a switch(`includeClosed`) to the
`GetUnmergedPullRequestsByHeadInfo` func to control whether the status
of PR must be open. In this case, by setting `includeClosed` to `true`,
we can query the closed PR.
![image](https://user-images.githubusercontent.com/33891828/222621045-bb80987c-10c5-4eac-aa0c-1fb9c6aefb51.png)
4. In the loop of comments, I use the`latestCloseCommentID` variable to
record the last occurrence of the close comment.
In the go template, if the status of PR is closed, the comments whose
type is `CommentTypePullRequestPush(29)` after `latestCloseCommentID`
won't be rendered.
![image](https://user-images.githubusercontent.com/33891828/222058913-c91cf3e3-819b-40c5-8015-654b31eeccff.png)
e.g.
1). The initial status of the PR is opened.
![image](https://user-images.githubusercontent.com/33891828/222453617-33c5093e-f712-4cd6-8489-9f87e2075869.png)
2). Then I click the button of `Close`. PR is closed now.
![image](https://user-images.githubusercontent.com/33891828/222453694-25c588a9-c121-4897-9ae5-0b13cf33d20b.png)
3). I try to push a commit to this PR, even though its current status is
closed.
![image](https://user-images.githubusercontent.com/33891828/222453916-361678fb-7321-410d-9e37-5a26e8095638.png)
But in comments list, this commit do not display.This is as expected :)
![image](https://user-images.githubusercontent.com/33891828/222454169-7617a791-78d2-404e-be5e-77d555f93313.png)
4). Click the `Reopen` button, the commit which is pushed after closing
PR display now.
![image](https://user-images.githubusercontent.com/33891828/222454533-897893b6-b96e-4701-b5cb-b1800f382b8f.png)
---------
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-03-03 16:16:58 +03:00
ctx . Data [ "LatestCloseCommentID" ] = latestCloseCommentID
2020-10-26 00:49:48 +03:00
// Combine multiple label assignments into a single comment
combineLabelComments ( issue )
2019-12-16 09:20:25 +03:00
getBranchData ( ctx , issue )
2016-12-25 18:27:25 +03:00
if issue . IsPull {
pull := issue . PullRequest
2018-12-12 02:49:33 +03:00
pull . Issue = issue
2016-12-25 19:19:25 +03:00
canDelete := false
2020-01-11 10:29:34 +03:00
ctx . Data [ "AllowMerge" ] = false
2016-12-25 19:19:25 +03:00
2017-06-21 04:00:03 +03:00
if ctx . IsSigned {
2022-11-19 11:12:33 +03:00
if err := pull . LoadHeadRepo ( ctx ) ; err != nil {
2020-03-03 01:31:55 +03:00
log . Error ( "LoadHeadRepo: %v" , err )
2022-04-28 18:45:33 +03:00
} else if pull . HeadRepo != nil {
2022-05-11 13:09:36 +03:00
perm , err := access_model . GetUserRepoPermission ( ctx , pull . HeadRepo , ctx . Doer )
2018-11-28 14:26:14 +03:00
if err != nil {
ctx . ServerError ( "GetUserRepoPermission" , err )
return
}
2021-11-09 22:57:58 +03:00
if perm . CanWrite ( unit . TypeCode ) {
2018-11-28 14:26:14 +03:00
// Check if branch is not protected
2022-04-28 18:45:33 +03:00
if pull . HeadBranch != pull . HeadRepo . DefaultBranch {
2023-01-16 11:00:22 +03:00
if protected , err := git_model . IsBranchProtected ( ctx , pull . HeadRepo . ID , pull . HeadBranch ) ; err != nil {
2022-04-28 18:45:33 +03:00
log . Error ( "IsProtectedBranch: %v" , err )
} else if ! protected {
canDelete = true
ctx . Data [ "DeleteBranchLink" ] = issue . Link ( ) + "/cleanup"
}
2018-11-28 14:26:14 +03:00
}
2022-04-28 18:45:33 +03:00
ctx . Data [ "CanWriteToHeadRepo" ] = true
2017-06-21 04:00:03 +03:00
}
2016-12-25 19:19:25 +03:00
}
2020-01-11 10:29:34 +03:00
2022-11-19 11:12:33 +03:00
if err := pull . LoadBaseRepo ( ctx ) ; err != nil {
2020-03-03 01:31:55 +03:00
log . Error ( "LoadBaseRepo: %v" , err )
2020-01-11 10:29:34 +03:00
}
2022-05-11 13:09:36 +03:00
perm , err := access_model . GetUserRepoPermission ( ctx , pull . BaseRepo , ctx . Doer )
2020-01-11 10:29:34 +03:00
if err != nil {
ctx . ServerError ( "GetUserRepoPermission" , err )
return
}
2022-05-03 22:46:28 +03:00
ctx . Data [ "AllowMerge" ] , err = pull_service . IsUserAllowedToMerge ( ctx , pull , perm , ctx . Doer )
2020-01-11 10:29:34 +03:00
if err != nil {
ctx . ServerError ( "IsUserAllowedToMerge" , err )
return
}
2020-04-18 16:50:25 +03:00
2022-06-13 12:37:59 +03:00
if ctx . Data [ "CanMarkConversation" ] , err = issues_model . CanMarkConversation ( issue , ctx . Doer ) ; err != nil {
2020-04-18 16:50:25 +03:00
ctx . ServerError ( "CanMarkConversation" , err )
return
}
2016-12-25 19:19:25 +03:00
}
2016-12-25 18:27:25 +03:00
2022-12-10 05:46:31 +03:00
prUnit , err := repo . GetUnit ( ctx , unit . TypePullRequests )
2018-01-05 21:56:50 +03:00
if err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "GetUnit" , err )
2018-01-05 21:56:50 +03:00
return
}
prConfig := prUnit . PullRequestsConfig ( )
2022-05-08 15:32:45 +03:00
var mergeStyle repo_model . MergeStyle
2018-01-05 21:56:50 +03:00
// Check correct values and select default
2021-12-10 04:27:50 +03:00
if ms , ok := ctx . Data [ "MergeStyle" ] . ( repo_model . MergeStyle ) ; ! ok ||
2018-01-05 21:56:50 +03:00
! prConfig . IsMergeStyleAllowed ( ms ) {
2021-03-27 17:55:40 +03:00
defaultMergeStyle := prConfig . GetDefaultMergeStyle ( )
if prConfig . IsMergeStyleAllowed ( defaultMergeStyle ) && ! ok {
2022-05-08 15:32:45 +03:00
mergeStyle = defaultMergeStyle
2021-03-27 17:55:40 +03:00
} else if prConfig . AllowMerge {
2022-05-08 15:32:45 +03:00
mergeStyle = repo_model . MergeStyleMerge
2018-01-05 21:56:50 +03:00
} else if prConfig . AllowRebase {
2022-05-08 15:32:45 +03:00
mergeStyle = repo_model . MergeStyleRebase
2018-12-27 13:27:08 +03:00
} else if prConfig . AllowRebaseMerge {
2022-05-08 15:32:45 +03:00
mergeStyle = repo_model . MergeStyleRebaseMerge
2018-01-05 21:56:50 +03:00
} else if prConfig . AllowSquash {
2022-05-08 15:32:45 +03:00
mergeStyle = repo_model . MergeStyleSquash
2021-03-04 06:41:23 +03:00
} else if prConfig . AllowManualMerge {
2022-05-08 15:32:45 +03:00
mergeStyle = repo_model . MergeStyleManuallyMerged
2018-01-05 21:56:50 +03:00
}
}
2022-05-08 15:32:45 +03:00
ctx . Data [ "MergeStyle" ] = mergeStyle
2022-12-29 15:40:20 +03:00
defaultMergeMessage , defaultMergeBody , err := pull_service . GetDefaultMergeMessage ( ctx , ctx . Repo . GitRepo , pull , mergeStyle )
2022-05-08 15:32:45 +03:00
if err != nil {
ctx . ServerError ( "GetDefaultMergeMessage" , err )
return
}
ctx . Data [ "DefaultMergeMessage" ] = defaultMergeMessage
2022-12-29 15:40:20 +03:00
ctx . Data [ "DefaultMergeBody" ] = defaultMergeBody
2022-05-08 15:32:45 +03:00
2022-12-29 15:40:20 +03:00
defaultSquashMergeMessage , defaultSquashMergeBody , err := pull_service . GetDefaultMergeMessage ( ctx , ctx . Repo . GitRepo , pull , repo_model . MergeStyleSquash )
2022-05-08 15:32:45 +03:00
if err != nil {
ctx . ServerError ( "GetDefaultSquashMergeMessage" , err )
return
}
ctx . Data [ "DefaultSquashMergeMessage" ] = defaultSquashMergeMessage
2022-12-29 15:40:20 +03:00
ctx . Data [ "DefaultSquashMergeBody" ] = defaultSquashMergeBody
2022-05-08 15:32:45 +03:00
2023-01-16 11:00:22 +03:00
pb , err := git_model . GetFirstMatchProtectedBranchRule ( ctx , pull . BaseRepoID , pull . BaseBranch )
if err != nil {
2018-12-11 14:28:37 +03:00
ctx . ServerError ( "LoadProtectedBranch" , err )
return
}
2021-10-17 19:58:36 +03:00
ctx . Data [ "ShowMergeInstructions" ] = true
2023-01-16 11:00:22 +03:00
if pb != nil {
pb . Repo = pull . BaseRepo
2021-11-09 16:14:12 +03:00
var showMergeInstructions bool
2022-03-22 10:03:22 +03:00
if ctx . Doer != nil {
2023-01-16 11:00:22 +03:00
showMergeInstructions = pb . CanUserPush ( ctx , ctx . Doer )
2021-11-09 16:14:12 +03:00
}
2023-01-16 11:00:22 +03:00
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
2020-10-13 21:50:57 +03:00
ctx . Data [ "ChangedProtectedFiles" ] = pull . ChangedProtectedFiles
ctx . Data [ "IsBlockedByChangedProtectedFiles" ] = len ( pull . ChangedProtectedFiles ) != 0
ctx . Data [ "ChangedProtectedFilesNum" ] = len ( pull . ChangedProtectedFiles )
2021-11-09 16:14:12 +03:00
ctx . Data [ "ShowMergeInstructions" ] = showMergeInstructions
2020-01-15 11:32:57 +03:00
}
ctx . Data [ "WillSign" ] = false
2022-03-22 10:03:22 +03:00
if ctx . Doer != nil {
sign , key , _ , err := asymkey_service . SignMerge ( ctx , pull , ctx . Doer , pull . BaseRepo . RepoPath ( ) , pull . BaseBranch , pull . GetGitRefName ( ) )
2020-01-15 11:32:57 +03:00
ctx . Data [ "WillSign" ] = sign
ctx . Data [ "SigningKey" ] = key
if err != nil {
2021-12-10 11:14:24 +03:00
if asymkey_service . IsErrWontSign ( err ) {
ctx . Data [ "WontSignReason" ] = err . ( * asymkey_service . ErrWontSign ) . Reason
2020-01-15 11:32:57 +03:00
} 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 )
}
}
2020-08-24 00:59:41 +03:00
} else {
ctx . Data [ "WontSignReason" ] = "not_signed_in"
2018-12-11 14:28:37 +03:00
}
2022-01-03 22:45:58 +03:00
isPullBranchDeletable := canDelete &&
2020-01-07 20:06:14 +03:00
pull . HeadRepo != nil &&
2021-11-30 23:06:32 +03:00
git . IsBranchExist ( ctx , pull . HeadRepo . RepoPath ( ) , pull . HeadBranch ) &&
2020-01-07 20:06:14 +03:00
( ! pull . HasMerged || ctx . Data [ "HeadBranchCommitID" ] == ctx . Data [ "PullHeadCommitID" ] )
2021-03-04 06:41:23 +03:00
2022-01-03 22:45:58 +03:00
if isPullBranchDeletable && pull . HasMerged {
2022-06-13 12:37:59 +03:00
exist , err := issues_model . HasUnmergedPullRequestsByHeadInfo ( ctx , pull . HeadRepoID , pull . HeadBranch )
2022-01-03 22:45:58 +03:00
if err != nil {
ctx . ServerError ( "HasUnmergedPullRequestsByHeadInfo" , err )
return
}
isPullBranchDeletable = ! exist
}
ctx . Data [ "IsPullBranchDeletable" ] = isPullBranchDeletable
2021-03-04 06:41:23 +03:00
stillCanManualMerge := func ( ) bool {
if pull . HasMerged || issue . IsClosed || ! ctx . IsSigned {
return false
}
if pull . CanAutoMerge ( ) || pull . IsWorkInProgress ( ) || pull . IsChecking ( ) {
return false
}
2022-03-22 10:03:22 +03:00
if ( ctx . Doer . IsAdmin || ctx . Repo . IsAdmin ( ) ) && prConfig . AllowManualMerge {
2021-03-04 06:41:23 +03:00
return true
}
return false
}
ctx . Data [ "StillCanManualMerge" ] = stillCanManualMerge ( )
2022-05-07 20:05:52 +03:00
// 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
}
2016-12-25 18:27:25 +03:00
}
2018-07-18 00:23:58 +03:00
// Get Dependencies
2023-03-28 20:23:25 +03:00
blockedBy , err := issue . BlockedByDependencies ( ctx , db . ListOptions { } )
2019-06-12 22:41:28 +03:00
if err != nil {
ctx . ServerError ( "BlockedByDependencies" , err )
return
}
2023-03-28 20:23:25 +03:00
ctx . Data [ "BlockedByDependencies" ] , ctx . Data [ "BlockedByDependenciesNotPermitted" ] = checkBlockedByIssues ( ctx , blockedBy )
if ctx . Written ( ) {
return
}
blocking , err := issue . BlockingDependencies ( ctx )
2019-06-12 22:41:28 +03:00
if err != nil {
ctx . ServerError ( "BlockingDependencies" , err )
return
}
2018-07-18 00:23:58 +03:00
2023-03-28 20:23:25 +03:00
ctx . Data [ "BlockingDependencies" ] , ctx . Data [ "BlockingByDependenciesNotPermitted" ] = checkBlockedByIssues ( ctx , blocking )
if ctx . Written ( ) {
return
}
2016-01-19 16:04:24 +03:00
ctx . Data [ "Participants" ] = participants
2016-02-02 04:55:12 +03:00
ctx . Data [ "NumParticipants" ] = len ( participants )
2014-07-26 10:28:04 +04:00
ctx . Data [ "Issue" ] = issue
2021-12-18 00:29:09 +03:00
ctx . Data [ "Reference" ] = issue . Ref
2021-11-16 21:18:25 +03:00
ctx . Data [ "SignInLink" ] = setting . AppSubURL + "/user/login?redirect_to=" + url . QueryEscape ( ctx . Data [ "Link" ] . ( string ) )
2022-03-22 10:03:22 +03:00
ctx . Data [ "IsIssuePoster" ] = ctx . IsSigned && issue . IsPoster ( ctx . Doer . ID )
2020-04-04 08:39:48 +03:00
ctx . Data [ "HasIssuesOrPullsWritePermission" ] = ctx . Repo . CanWriteIssuesOrPulls ( issue . IsPull )
2021-11-09 22:57:58 +03:00
ctx . Data [ "HasProjectsWritePermission" ] = ctx . Repo . CanWrite ( unit . TypeProjects )
2022-03-22 10:03:22 +03:00
ctx . Data [ "IsRepoAdmin" ] = ctx . IsSigned && ( ctx . Repo . IsAdmin ( ) || ctx . Doer . IsAdmin )
2019-02-18 23:55:04 +03:00
ctx . Data [ "LockReasons" ] = setting . Repository . Issue . LockReasons
2020-05-15 01:55:43 +03:00
ctx . Data [ "RefEndName" ] = git . RefEndName ( issue . Ref )
2022-01-21 20:59:26 +03:00
var hiddenCommentTypes * big . Int
if ctx . IsSigned {
2022-03-22 10:03:22 +03:00
val , err := user_model . GetUserSetting ( ctx . Doer . ID , user_model . SettingsKeyHiddenCommentTypes )
2022-01-21 20:59:26 +03:00
if err != nil {
ctx . ServerError ( "GetUserSetting" , err )
return
}
hiddenCommentTypes , _ = new ( big . Int ) . SetString ( val , 10 ) // we can safely ignore the failed conversion here
}
2022-06-13 12:37:59 +03:00
ctx . Data [ "ShouldShowCommentType" ] = func ( commentType issues_model . CommentType ) bool {
2022-01-21 20:59:26 +03:00
return hiddenCommentTypes == nil || hiddenCommentTypes . Bit ( int ( commentType ) ) == 0
}
2021-04-05 18:30:52 +03:00
ctx . HTML ( http . StatusOK , tplIssueView )
2014-07-26 10:28:04 +04:00
}
2023-03-28 20:23:25 +03:00
func checkBlockedByIssues ( ctx * context . Context , blockers [ ] * issues_model . DependencyInfo ) ( canRead , notPermitted [ ] * issues_model . DependencyInfo ) {
var lastRepoID int64
var lastPerm access_model . Permission
for i , blocker := range blockers {
// Get the permissions for this repository
perm := lastPerm
if lastRepoID != blocker . Repository . ID {
if blocker . Repository . ID == ctx . Repo . Repository . ID {
perm = ctx . Repo . Permission
} else {
var err error
perm , err = access_model . GetUserRepoPermission ( ctx , & blocker . Repository , ctx . Doer )
if err != nil {
ctx . ServerError ( "GetUserRepoPermission" , err )
return
}
}
lastRepoID = blocker . Repository . ID
}
// check permission
if ! perm . CanReadIssuesOrPulls ( blocker . Issue . IsPull ) {
blockers [ len ( notPermitted ) ] , blockers [ i ] = blocker , blockers [ len ( notPermitted ) ]
notPermitted = blockers [ : len ( notPermitted ) + 1 ]
}
}
blockers = blockers [ len ( notPermitted ) : ]
sortDependencyInfo ( blockers )
sortDependencyInfo ( notPermitted )
return blockers , 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
} )
}
2017-09-12 09:48:13 +03:00
// GetActionIssue will return the issue which is used in the context.
2022-06-13 12:37:59 +03:00
func GetActionIssue ( ctx * context . Context ) * issues_model . Issue {
issue , err := issues_model . GetIssueByIndex ( ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2014-07-26 10:28:04 +04:00
if err != nil {
2022-06-13 12:37:59 +03:00
ctx . NotFoundOrServerError ( "GetIssueByIndex" , issues_model . IsErrIssueNotExist , err )
2017-10-16 10:55:43 +03:00
return nil
}
2018-12-13 18:55:43 +03:00
issue . Repo = ctx . Repo . Repository
2017-12-04 02:14:26 +03:00
checkIssueRights ( ctx , issue )
if ctx . Written ( ) {
2017-10-16 10:55:43 +03:00
return nil
}
2022-06-13 12:37:59 +03:00
if err = issue . LoadAttributes ( ctx ) ; err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "LoadAttributes" , nil )
2015-08-19 18:14:57 +03:00
return nil
}
return issue
}
2022-06-13 12:37:59 +03:00
func checkIssueRights ( ctx * context . Context , issue * issues_model . Issue ) {
2021-11-09 22:57:58 +03:00
if issue . IsPull && ! ctx . Repo . CanRead ( unit . TypePullRequests ) ||
! issue . IsPull && ! ctx . Repo . CanRead ( unit . TypeIssues ) {
2018-01-11 00:34:17 +03:00
ctx . NotFound ( "IssueOrPullRequestUnitNotAllowed" , nil )
2017-12-04 02:14:26 +03:00
}
}
2022-06-13 12:37:59 +03:00
func getActionIssues ( ctx * context . Context ) [ ] * issues_model . Issue {
2021-08-11 03:31:13 +03:00
commaSeparatedIssueIDs := ctx . FormString ( "issue_ids" )
2017-03-15 04:10:35 +03:00
if len ( commaSeparatedIssueIDs ) == 0 {
return nil
}
issueIDs := make ( [ ] int64 , 0 , 10 )
for _ , stringIssueID := range strings . Split ( commaSeparatedIssueIDs , "," ) {
issueID , err := strconv . ParseInt ( stringIssueID , 10 , 64 )
if err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "ParseInt" , err )
2017-03-15 04:10:35 +03:00
return nil
}
issueIDs = append ( issueIDs , issueID )
}
2022-06-13 12:37:59 +03:00
issues , err := issues_model . GetIssuesByIDs ( ctx , issueIDs )
2017-03-15 04:10:35 +03:00
if err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "GetIssuesByIDs" , err )
2017-03-15 04:10:35 +03:00
return nil
}
2017-10-16 10:55:43 +03:00
// Check access rights for all issues
2021-11-09 22:57:58 +03:00
issueUnitEnabled := ctx . Repo . CanRead ( unit . TypeIssues )
prUnitEnabled := ctx . Repo . CanRead ( unit . TypePullRequests )
2017-10-16 10:55:43 +03:00
for _ , issue := range issues {
2022-06-30 18:55:08 +03:00
if issue . RepoID != ctx . Repo . Repository . ID {
ctx . NotFound ( "some issue's RepoID is incorrect" , errors . New ( "some issue's RepoID is incorrect" ) )
return nil
}
2017-10-16 10:55:43 +03:00
if issue . IsPull && ! prUnitEnabled || ! issue . IsPull && ! issueUnitEnabled {
2018-01-11 00:34:17 +03:00
ctx . NotFound ( "IssueOrPullRequestUnitNotAllowed" , nil )
2017-10-16 10:55:43 +03:00
return nil
}
2022-06-13 12:37:59 +03:00
if err = issue . LoadAttributes ( ctx ) ; err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "LoadAttributes" , err )
2017-10-16 10:55:43 +03:00
return nil
}
}
2017-03-15 04:10:35 +03:00
return issues
}
2022-04-07 21:59:56 +03:00
// GetIssueInfo get an issue of a repository
func GetIssueInfo ( ctx * context . Context ) {
2022-06-13 12:37:59 +03:00
issue , err := issues_model . GetIssueWithAttrsByIndex ( ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2022-04-07 21:59:56 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrIssueNotExist ( err ) {
2022-04-07 21:59:56 +03:00
ctx . Error ( http . StatusNotFound )
} else {
ctx . Error ( http . StatusInternalServerError , "GetIssueByIndex" , err . Error ( ) )
}
return
}
2022-06-04 23:10:54 +03:00
if issue . IsPull {
// Need to check if Pulls are enabled and we can read Pulls
if ! ctx . Repo . Repository . CanEnablePulls ( ) || ! ctx . Repo . CanRead ( unit . TypePullRequests ) {
ctx . Error ( http . StatusNotFound )
return
}
} else {
// Need to check if Issues are enabled and we can read Issues
if ! ctx . Repo . CanRead ( unit . TypeIssues ) {
ctx . Error ( http . StatusNotFound )
return
}
}
2022-11-19 11:12:33 +03:00
ctx . JSON ( http . StatusOK , convert . ToAPIIssue ( ctx , issue ) )
2022-04-07 21:59:56 +03:00
}
2016-11-24 10:04:31 +03:00
// UpdateIssueTitle change issue's title
2016-03-11 19:56:52 +03:00
func UpdateIssueTitle ( ctx * context . Context ) {
2017-09-12 09:48:13 +03:00
issue := GetActionIssue ( ctx )
2015-08-19 18:14:57 +03:00
if ctx . Written ( ) {
2014-07-26 10:28:04 +04:00
return
}
2022-03-22 10:03:22 +03:00
if ! ctx . IsSigned || ( ! issue . IsPoster ( ctx . Doer . ID ) && ! ctx . Repo . CanWriteIssuesOrPulls ( issue . IsPull ) ) {
2021-04-05 18:30:52 +03:00
ctx . Error ( http . StatusForbidden )
2014-07-26 10:28:04 +04:00
return
}
2021-07-29 04:42:15 +03:00
title := ctx . FormTrim ( "title" )
2016-08-14 13:32:24 +03:00
if len ( title ) == 0 {
2021-04-05 18:30:52 +03:00
ctx . Error ( http . StatusNoContent )
2015-08-19 18:14:57 +03:00
return
2014-07-26 10:28:04 +04:00
}
2015-08-19 18:14:57 +03:00
2023-04-14 21:18:28 +03:00
if err := issue_service . ChangeTitle ( ctx , issue , ctx . Doer , title ) ; err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "ChangeTitle" , err )
2014-07-26 10:28:04 +04:00
return
}
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , map [ string ] interface { } {
2016-08-14 13:32:24 +03:00
"title" : issue . Title ,
2014-07-26 10:28:04 +04:00
} )
}
2020-09-08 19:29:51 +03:00
// UpdateIssueRef change issue's ref (branch)
func UpdateIssueRef ( ctx * context . Context ) {
issue := GetActionIssue ( ctx )
if ctx . Written ( ) {
return
}
2022-03-22 10:03:22 +03:00
if ! ctx . IsSigned || ( ! issue . IsPoster ( ctx . Doer . ID ) && ! ctx . Repo . CanWriteIssuesOrPulls ( issue . IsPull ) ) || issue . IsPull {
2021-04-05 18:30:52 +03:00
ctx . Error ( http . StatusForbidden )
2020-09-08 19:29:51 +03:00
return
}
2021-07-29 04:42:15 +03:00
ref := ctx . FormTrim ( "ref" )
2020-09-08 19:29:51 +03:00
2023-04-14 21:18:28 +03:00
if err := issue_service . ChangeIssueRef ( ctx , issue , ctx . Doer , ref ) ; err != nil {
2020-09-08 19:29:51 +03:00
ctx . ServerError ( "ChangeRef" , err )
return
}
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , map [ string ] interface { } {
2020-09-08 19:29:51 +03:00
"ref" : ref ,
} )
}
2016-11-24 10:04:31 +03:00
// UpdateIssueContent change issue's content
2016-03-11 19:56:52 +03:00
func UpdateIssueContent ( ctx * context . Context ) {
2017-09-12 09:48:13 +03:00
issue := GetActionIssue ( ctx )
2015-08-19 23:31:28 +03:00
if ctx . Written ( ) {
return
}
2022-03-22 10:03:22 +03:00
if ! ctx . IsSigned || ( ctx . Doer . ID != issue . PosterID && ! ctx . Repo . CanWriteIssuesOrPulls ( issue . IsPull ) ) {
2021-04-05 18:30:52 +03:00
ctx . Error ( http . StatusForbidden )
2015-08-19 23:31:28 +03:00
return
}
2022-03-22 10:03:22 +03:00
if err := issue_service . ChangeContent ( issue , ctx . Doer , ctx . Req . FormValue ( "content" ) ) ; err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "ChangeContent" , err )
2015-08-19 23:31:28 +03:00
return
}
2021-08-21 16:04:47 +03:00
// when update the request doesn't intend to update attachments (eg: change checkbox state), ignore attachment updates
if ! ctx . FormBool ( "ignore_attachments" ) {
2022-05-20 17:08:52 +03:00
if err := updateAttachments ( ctx , issue , ctx . FormStrings ( "files[]" ) ) ; err != nil {
2021-08-21 16:04:47 +03:00
ctx . ServerError ( "UpdateAttachments" , err )
return
}
2021-04-20 01:25:08 +03:00
}
content , err := markdown . RenderString ( & markup . RenderContext {
2021-11-16 21:18:25 +03:00
URLPrefix : ctx . FormString ( "context" ) , // FIXME: <- IS THIS SAFE ?
2021-04-20 01:25:08 +03:00
Metas : ctx . Repo . Repository . ComposeMetas ( ) ,
2021-06-21 01:39:12 +03:00
GitRepo : ctx . Repo . GitRepo ,
2021-08-28 23:15:56 +03:00
Ctx : ctx ,
2021-04-20 01:25:08 +03:00
} , issue . Content )
if err != nil {
ctx . ServerError ( "RenderString" , err )
return
2019-10-15 15:19:32 +03:00
}
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , map [ string ] interface { } {
2021-04-20 01:25:08 +03:00
"content" : content ,
2020-12-13 22:12:27 +03:00
"attachments" : attachmentsHTML ( ctx , issue . Attachments , issue . Content ) ,
2015-08-19 23:31:28 +03:00
} )
}
2022-04-07 21:59:56 +03:00
// UpdateIssueDeadline updates an issue deadline
func UpdateIssueDeadline ( ctx * context . Context ) {
form := web . GetForm ( ctx ) . ( * api . EditDeadlineOption )
2022-06-13 12:37:59 +03:00
issue , err := issues_model . GetIssueByIndex ( ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2022-04-07 21:59:56 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrIssueNotExist ( err ) {
2022-04-07 21:59:56 +03:00
ctx . NotFound ( "GetIssueByIndex" , err )
} else {
ctx . Error ( http . StatusInternalServerError , "GetIssueByIndex" , err . Error ( ) )
}
return
}
if ! ctx . Repo . CanWriteIssuesOrPulls ( issue . IsPull ) {
ctx . Error ( http . StatusForbidden , "" , "Not repo writer" )
return
}
var deadlineUnix timeutil . TimeStamp
var deadline time . Time
if form . Deadline != nil && ! form . Deadline . IsZero ( ) {
deadline = time . Date ( form . Deadline . Year ( ) , form . Deadline . Month ( ) , form . Deadline . Day ( ) ,
23 , 59 , 59 , 0 , time . Local )
deadlineUnix = timeutil . TimeStamp ( deadline . Unix ( ) )
}
2022-06-13 12:37:59 +03:00
if err := issues_model . UpdateIssueDeadline ( issue , deadlineUnix , ctx . Doer ) ; err != nil {
2022-04-07 21:59:56 +03:00
ctx . Error ( http . StatusInternalServerError , "UpdateIssueDeadline" , err . Error ( ) )
return
}
ctx . JSON ( http . StatusCreated , api . IssueDeadline { Deadline : & deadline } )
}
2016-11-24 10:04:31 +03:00
// UpdateIssueMilestone change issue's milestone
2016-03-11 19:56:52 +03:00
func UpdateIssueMilestone ( ctx * context . Context ) {
2017-03-15 04:10:35 +03:00
issues := getActionIssues ( ctx )
2015-08-14 19:42:43 +03:00
if ctx . Written ( ) {
2014-07-26 10:28:04 +04:00
return
}
2021-07-29 04:42:15 +03:00
milestoneID := ctx . FormInt64 ( "id" )
2017-03-15 04:10:35 +03:00
for _ , issue := range issues {
oldMilestoneID := issue . MilestoneID
if oldMilestoneID == milestoneID {
continue
}
issue . MilestoneID = milestoneID
2022-03-22 10:03:22 +03:00
if err := issue_service . ChangeMilestoneAssign ( issue , ctx . Doer , oldMilestoneID ) ; err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "ChangeMilestoneAssign" , err )
2017-03-15 04:10:35 +03:00
return
}
2014-07-26 10:28:04 +04:00
}
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , map [ string ] interface { } {
2014-07-26 10:28:04 +04:00
"ok" : true ,
} )
}
2019-10-25 17:46:37 +03:00
// UpdateIssueAssignee change issue's or pull's assignee
2016-03-11 19:56:52 +03:00
func UpdateIssueAssignee ( ctx * context . Context ) {
2017-03-15 04:10:35 +03:00
issues := getActionIssues ( ctx )
2015-08-14 19:42:43 +03:00
if ctx . Written ( ) {
2014-07-26 10:28:04 +04:00
return
}
2021-07-29 04:42:15 +03:00
assigneeID := ctx . FormInt64 ( "id" )
2021-08-11 03:31:13 +03:00
action := ctx . FormString ( "action" )
2018-05-09 19:29:04 +03:00
2017-03-15 04:10:35 +03:00
for _ , issue := range issues {
2018-05-09 19:29:04 +03:00
switch action {
case "clear" :
2023-04-14 21:18:28 +03:00
if err := issue_service . DeleteNotPassedAssignee ( ctx , issue , ctx . Doer , [ ] * user_model . User { } ) ; err != nil {
2018-05-09 19:29:04 +03:00
ctx . ServerError ( "ClearAssignees" , err )
return
}
default :
2022-12-03 05:48:26 +03:00
assignee , err := user_model . GetUserByID ( ctx , assigneeID )
2019-10-25 17:46:37 +03:00
if err != nil {
ctx . ServerError ( "GetUserByID" , err )
return
}
2022-05-11 13:09:36 +03:00
valid , err := access_model . CanBeAssigned ( ctx , assignee , issue . Repo , issue . IsPull )
2019-10-25 17:46:37 +03:00
if err != nil {
ctx . ServerError ( "canBeAssigned" , err )
2018-05-09 19:29:04 +03:00
return
}
2019-10-25 17:46:37 +03:00
if ! valid {
2022-06-13 12:37:59 +03:00
ctx . ServerError ( "canBeAssigned" , repo_model . ErrUserDoesNotHaveAccessToRepo { UserID : assigneeID , RepoName : issue . Repo . Name } )
2019-10-25 17:46:37 +03:00
return
}
2023-04-14 21:18:28 +03:00
_ , _ , err = issue_service . ToggleAssignee ( ctx , issue , ctx . Doer , assigneeID )
2019-10-25 17:46:37 +03:00
if err != nil {
ctx . ServerError ( "ToggleAssignee" , err )
return
}
2017-03-15 04:10:35 +03:00
}
2014-07-26 10:28:04 +04:00
}
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , map [ string ] interface { } {
2017-03-15 04:10:35 +03:00
"ok" : true ,
} )
}
2014-07-26 10:28:04 +04:00
2020-10-12 22:55:13 +03:00
// UpdatePullReviewRequest add or remove review request
func UpdatePullReviewRequest ( ctx * context . Context ) {
2020-04-06 19:33:34 +03:00
issues := getActionIssues ( ctx )
if ctx . Written ( ) {
return
}
2021-07-29 04:42:15 +03:00
reviewID := ctx . FormInt64 ( "id" )
2021-08-11 03:31:13 +03:00
action := ctx . FormString ( "action" )
2020-04-06 19:33:34 +03:00
2020-09-02 19:55:13 +03:00
// TODO: Not support 'clear' now
if action != "attach" && action != "detach" {
2022-03-23 07:54:07 +03:00
ctx . Status ( http . StatusForbidden )
2020-04-06 19:33:34 +03:00
return
}
for _ , issue := range issues {
2022-04-08 12:11:15 +03:00
if err := issue . LoadRepo ( ctx ) ; err != nil {
2020-10-12 22:55:13 +03:00
ctx . ServerError ( "issue.LoadRepo" , err )
return
}
if ! issue . IsPull {
log . Warn (
"UpdatePullReviewRequest: refusing to add review request for non-PR issue %-v#%d" ,
issue . Repo , issue . Index ,
)
2022-03-23 07:54:07 +03:00
ctx . Status ( http . StatusForbidden )
2020-10-12 22:55:13 +03:00
return
}
if reviewID < 0 {
// negative reviewIDs represent team requests
2023-02-18 15:11:03 +03:00
if err := issue . Repo . LoadOwner ( ctx ) ; err != nil {
ctx . ServerError ( "issue.Repo.LoadOwner" , err )
2020-10-12 22:55:13 +03:00
return
}
if ! issue . Repo . Owner . IsOrganization ( ) {
log . Warn (
"UpdatePullReviewRequest: refusing to add team review request for %s#%d owned by non organization UID[%d]" ,
issue . Repo . FullName ( ) , issue . Index , issue . Repo . ID ,
)
2022-03-23 07:54:07 +03:00
ctx . Status ( http . StatusForbidden )
2020-10-12 22:55:13 +03:00
return
}
2020-04-06 19:33:34 +03:00
2022-05-20 17:08:52 +03:00
team , err := organization . GetTeamByID ( ctx , - reviewID )
2020-04-06 19:33:34 +03:00
if err != nil {
2022-03-29 09:29:02 +03:00
ctx . ServerError ( "GetTeamByID" , err )
2020-04-06 19:33:34 +03:00
return
}
2020-10-12 22:55:13 +03:00
if team . OrgID != issue . Repo . OwnerID {
log . Warn (
"UpdatePullReviewRequest: refusing to add team review request for UID[%d] team %s to %s#%d owned by UID[%d]" ,
team . OrgID , team . Name , issue . Repo . FullName ( ) , issue . Index , issue . Repo . ID )
2022-03-23 07:54:07 +03:00
ctx . Status ( http . StatusForbidden )
2020-10-12 22:55:13 +03:00
return
}
2022-04-28 14:48:48 +03:00
err = issue_service . IsValidTeamReviewRequest ( ctx , team , ctx . Doer , action == "attach" , issue )
2020-04-06 19:33:34 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrNotValidReviewRequest ( err ) {
2020-10-12 22:55:13 +03:00
log . Warn (
"UpdatePullReviewRequest: refusing to add invalid team review request for UID[%d] team %s to %s#%d owned by UID[%d]: Error: %v" ,
team . OrgID , team . Name , issue . Repo . FullName ( ) , issue . Index , issue . Repo . ID ,
err ,
)
2022-03-23 07:54:07 +03:00
ctx . Status ( http . StatusForbidden )
2020-10-12 22:55:13 +03:00
return
}
2020-10-20 21:18:25 +03:00
ctx . ServerError ( "IsValidTeamReviewRequest" , err )
2020-04-06 19:33:34 +03:00
return
}
2023-04-14 21:18:28 +03:00
_ , err = issue_service . TeamReviewRequest ( ctx , issue , ctx . Doer , team , action == "attach" )
2020-04-06 19:33:34 +03:00
if err != nil {
2020-10-12 22:55:13 +03:00
ctx . ServerError ( "TeamReviewRequest" , err )
2020-04-06 19:33:34 +03:00
return
}
2020-10-12 22:55:13 +03:00
continue
}
2022-12-03 05:48:26 +03:00
reviewer , err := user_model . GetUserByID ( ctx , reviewID )
2020-10-12 22:55:13 +03:00
if err != nil {
2021-11-24 12:49:20 +03:00
if user_model . IsErrUserNotExist ( err ) {
2020-10-12 22:55:13 +03:00
log . Warn (
"UpdatePullReviewRequest: requested reviewer [%d] for %-v to %-v#%d is not exist: Error: %v" ,
reviewID , issue . Repo , issue . Index ,
err ,
)
2022-03-23 07:54:07 +03:00
ctx . Status ( http . StatusForbidden )
2020-10-12 22:55:13 +03:00
return
}
ctx . ServerError ( "GetUserByID" , err )
return
}
2022-04-28 14:48:48 +03:00
err = issue_service . IsValidReviewRequest ( ctx , reviewer , ctx . Doer , action == "attach" , issue , nil )
2020-10-12 22:55:13 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrNotValidReviewRequest ( err ) {
2020-10-12 22:55:13 +03:00
log . Warn (
"UpdatePullReviewRequest: refusing to add invalid review request for %-v to %-v#%d: Error: %v" ,
reviewer , issue . Repo , issue . Index ,
err ,
)
2022-03-23 07:54:07 +03:00
ctx . Status ( http . StatusForbidden )
2020-10-12 22:55:13 +03:00
return
}
ctx . ServerError ( "isValidReviewRequest" , err )
return
}
2023-04-14 21:18:28 +03:00
_ , err = issue_service . ReviewRequest ( ctx , issue , ctx . Doer , reviewer , action == "attach" )
2020-10-12 22:55:13 +03:00
if err != nil {
ctx . ServerError ( "ReviewRequest" , err )
2020-09-02 19:55:13 +03:00
return
2020-04-06 19:33:34 +03:00
}
}
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , map [ string ] interface { } {
2020-04-06 19:33:34 +03:00
"ok" : true ,
} )
}
2022-04-07 21:59:56 +03:00
// SearchIssues searches for issues across the repositories that the user has access to
func SearchIssues ( ctx * context . Context ) {
before , since , err := context . GetQueryBeforeSince ( ctx )
if err != nil {
ctx . Error ( http . StatusUnprocessableEntity , err . Error ( ) )
return
}
var isClosed util . OptionalBool
switch ctx . FormString ( "state" ) {
case "closed" :
isClosed = util . OptionalBoolTrue
case "all" :
isClosed = util . OptionalBoolNone
default :
isClosed = util . OptionalBoolFalse
}
// find repos user can access (for issue search)
2022-06-06 11:01:49 +03:00
opts := & repo_model . SearchRepoOptions {
2022-04-07 21:59:56 +03:00
Private : false ,
AllPublic : true ,
TopicOnly : false ,
Collaborate : util . OptionalBoolNone ,
// This needs to be a column that is not nil in fixtures or
// MySQL will return different results when sorting by null in some cases
OrderBy : db . SearchOrderByAlphabetically ,
Actor : ctx . Doer ,
}
if ctx . IsSigned {
opts . Private = true
opts . AllLimited = true
}
if ctx . FormString ( "owner" ) != "" {
2022-05-20 17:08:52 +03:00
owner , err := user_model . GetUserByName ( ctx , ctx . FormString ( "owner" ) )
2022-04-07 21:59:56 +03:00
if err != nil {
if user_model . IsErrUserNotExist ( err ) {
ctx . Error ( http . StatusBadRequest , "Owner not found" , err . Error ( ) )
} else {
ctx . Error ( http . StatusInternalServerError , "GetUserByName" , err . Error ( ) )
}
return
}
opts . OwnerID = owner . ID
opts . AllLimited = false
opts . AllPublic = false
opts . Collaborate = util . OptionalBoolFalse
}
if ctx . FormString ( "team" ) != "" {
if ctx . FormString ( "owner" ) == "" {
ctx . Error ( http . StatusBadRequest , "" , "Owner organisation is required for filtering on team" )
return
}
2022-05-20 17:08:52 +03:00
team , err := organization . GetTeam ( ctx , opts . OwnerID , ctx . FormString ( "team" ) )
2022-04-07 21:59:56 +03:00
if err != nil {
if organization . IsErrTeamNotExist ( err ) {
ctx . Error ( http . StatusBadRequest , "Team not found" , err . Error ( ) )
} else {
ctx . Error ( http . StatusInternalServerError , "GetUserByName" , err . Error ( ) )
}
return
}
opts . TeamID = team . ID
}
2022-06-06 11:01:49 +03:00
repoCond := repo_model . SearchRepositoryCondition ( opts )
repoIDs , _ , err := repo_model . SearchRepositoryIDs ( opts )
2022-04-07 21:59:56 +03:00
if err != nil {
2022-11-19 11:12:33 +03:00
ctx . Error ( http . StatusInternalServerError , "SearchRepositoryIDs" , err . Error ( ) )
2022-04-07 21:59:56 +03:00
return
}
2022-06-13 12:37:59 +03:00
var issues [ ] * issues_model . Issue
2022-04-07 21:59:56 +03:00
var filteredCount int64
keyword := ctx . FormTrim ( "q" )
if strings . IndexByte ( keyword , 0 ) >= 0 {
keyword = ""
}
var issueIDs [ ] int64
if len ( keyword ) > 0 && len ( repoIDs ) > 0 {
if issueIDs , err = issue_indexer . SearchIssuesByKeyword ( ctx , repoIDs , keyword ) ; err != nil {
ctx . Error ( http . StatusInternalServerError , "SearchIssuesByKeyword" , err . Error ( ) )
return
}
}
var isPull util . OptionalBool
switch ctx . FormString ( "type" ) {
case "pulls" :
isPull = util . OptionalBoolTrue
case "issues" :
isPull = util . OptionalBoolFalse
default :
isPull = util . OptionalBoolNone
}
labels := ctx . FormTrim ( "labels" )
var includedLabelNames [ ] string
if len ( labels ) > 0 {
includedLabelNames = strings . Split ( labels , "," )
}
milestones := ctx . FormTrim ( "milestones" )
var includedMilestones [ ] string
if len ( milestones ) > 0 {
includedMilestones = strings . Split ( milestones , "," )
}
2023-01-29 06:45:29 +03:00
projectID := ctx . FormInt64 ( "project" )
2022-04-07 21:59:56 +03:00
// this api is also used in UI,
// so the default limit is set to fit UI needs
limit := ctx . FormInt ( "limit" )
if limit == 0 {
limit = setting . UI . IssuePagingNum
} else if limit > setting . API . MaxResponseItems {
limit = setting . API . MaxResponseItems
}
// Only fetch the issues if we either don't have a keyword or the search returned issues
// This would otherwise return all issues if no issues were found by the search.
if len ( keyword ) == 0 || len ( issueIDs ) > 0 || len ( includedLabelNames ) > 0 || len ( includedMilestones ) > 0 {
2022-06-13 12:37:59 +03:00
issuesOpt := & issues_model . IssuesOptions {
2022-04-07 21:59:56 +03:00
ListOptions : db . ListOptions {
Page : ctx . FormInt ( "page" ) ,
PageSize : limit ,
} ,
2022-04-25 17:06:24 +03:00
RepoCond : repoCond ,
2022-04-07 21:59:56 +03:00
IsClosed : isClosed ,
IssueIDs : issueIDs ,
IncludedLabelNames : includedLabelNames ,
IncludeMilestones : includedMilestones ,
2023-01-29 06:45:29 +03:00
ProjectID : projectID ,
2022-04-07 21:59:56 +03:00
SortType : "priorityrepo" ,
PriorityRepoID : ctx . FormInt64 ( "priority_repo_id" ) ,
IsPull : isPull ,
UpdatedBeforeUnix : before ,
UpdatedAfterUnix : since ,
}
ctxUserID := int64 ( 0 )
if ctx . IsSigned {
ctxUserID = ctx . Doer . ID
}
// Filter for: Created by User, Assigned to User, Mentioning User, Review of User Requested
if ctx . FormBool ( "created" ) {
issuesOpt . PosterID = ctxUserID
}
if ctx . FormBool ( "assigned" ) {
issuesOpt . AssigneeID = ctxUserID
}
if ctx . FormBool ( "mentioned" ) {
issuesOpt . MentionedID = ctxUserID
}
if ctx . FormBool ( "review_requested" ) {
issuesOpt . ReviewRequestedID = ctxUserID
}
2023-02-25 05:55:50 +03:00
if ctx . FormBool ( "reviewed" ) {
issuesOpt . ReviewedID = ctxUserID
}
2022-04-07 21:59:56 +03:00
2022-11-19 11:12:33 +03:00
if issues , err = issues_model . Issues ( ctx , issuesOpt ) ; err != nil {
2022-04-07 21:59:56 +03:00
ctx . Error ( http . StatusInternalServerError , "Issues" , err . Error ( ) )
return
}
issuesOpt . ListOptions = db . ListOptions {
Page : - 1 ,
}
2022-11-19 11:12:33 +03:00
if filteredCount , err = issues_model . CountIssues ( ctx , issuesOpt ) ; err != nil {
2022-04-07 21:59:56 +03:00
ctx . Error ( http . StatusInternalServerError , "CountIssues" , err . Error ( ) )
return
}
}
ctx . SetTotalCountHeader ( filteredCount )
2022-11-19 11:12:33 +03:00
ctx . JSON ( http . StatusOK , convert . ToAPIIssueList ( ctx , issues ) )
2022-04-07 21:59:56 +03:00
}
func getUserIDForFilter ( ctx * context . Context , queryName string ) int64 {
userName := ctx . FormString ( queryName )
if len ( userName ) == 0 {
return 0
}
2022-05-20 17:08:52 +03:00
user , err := user_model . GetUserByName ( ctx , userName )
2022-04-07 21:59:56 +03:00
if user_model . IsErrUserNotExist ( err ) {
ctx . NotFound ( "" , err )
return 0
}
if err != nil {
ctx . Error ( http . StatusInternalServerError , err . Error ( ) )
return 0
}
return user . ID
}
// ListIssues list the issues of a repository
func ListIssues ( ctx * context . Context ) {
before , since , err := context . GetQueryBeforeSince ( ctx )
if err != nil {
ctx . Error ( http . StatusUnprocessableEntity , err . Error ( ) )
return
}
var isClosed util . OptionalBool
switch ctx . FormString ( "state" ) {
case "closed" :
isClosed = util . OptionalBoolTrue
case "all" :
isClosed = util . OptionalBoolNone
default :
isClosed = util . OptionalBoolFalse
}
2022-06-13 12:37:59 +03:00
var issues [ ] * issues_model . Issue
2022-04-07 21:59:56 +03:00
var filteredCount int64
keyword := ctx . FormTrim ( "q" )
if strings . IndexByte ( keyword , 0 ) >= 0 {
keyword = ""
}
var issueIDs [ ] int64
var labelIDs [ ] int64
if len ( keyword ) > 0 {
issueIDs , err = issue_indexer . SearchIssuesByKeyword ( ctx , [ ] int64 { ctx . Repo . Repository . ID } , keyword )
if err != nil {
ctx . Error ( http . StatusInternalServerError , err . Error ( ) )
return
}
}
if splitted := strings . Split ( ctx . FormString ( "labels" ) , "," ) ; len ( splitted ) > 0 {
2022-06-13 12:37:59 +03:00
labelIDs , err = issues_model . GetLabelIDsInRepoByNames ( ctx . Repo . Repository . ID , splitted )
2022-04-07 21:59:56 +03:00
if err != nil {
ctx . Error ( http . StatusInternalServerError , err . Error ( ) )
return
}
}
var mileIDs [ ] int64
if part := strings . Split ( ctx . FormString ( "milestones" ) , "," ) ; len ( part ) > 0 {
for i := range part {
// uses names and fall back to ids
// non existent milestones are discarded
2022-04-08 12:11:15 +03:00
mile , err := issues_model . GetMilestoneByRepoIDANDName ( ctx . Repo . Repository . ID , part [ i ] )
2022-04-07 21:59:56 +03:00
if err == nil {
mileIDs = append ( mileIDs , mile . ID )
continue
}
2022-04-08 12:11:15 +03:00
if ! issues_model . IsErrMilestoneNotExist ( err ) {
2022-04-07 21:59:56 +03:00
ctx . Error ( http . StatusInternalServerError , err . Error ( ) )
return
}
id , err := strconv . ParseInt ( part [ i ] , 10 , 64 )
if err != nil {
continue
}
2022-04-08 12:11:15 +03:00
mile , err = issues_model . GetMilestoneByRepoID ( ctx , ctx . Repo . Repository . ID , id )
2022-04-07 21:59:56 +03:00
if err == nil {
mileIDs = append ( mileIDs , mile . ID )
continue
}
2022-04-08 12:11:15 +03:00
if issues_model . IsErrMilestoneNotExist ( err ) {
2022-04-07 21:59:56 +03:00
continue
}
ctx . Error ( http . StatusInternalServerError , err . Error ( ) )
}
}
2023-01-29 06:45:29 +03:00
projectID := ctx . FormInt64 ( "project" )
2022-04-07 21:59:56 +03:00
listOptions := db . ListOptions {
Page : ctx . FormInt ( "page" ) ,
PageSize : convert . ToCorrectPageSize ( ctx . FormInt ( "limit" ) ) ,
}
var isPull util . OptionalBool
switch ctx . FormString ( "type" ) {
case "pulls" :
isPull = util . OptionalBoolTrue
case "issues" :
isPull = util . OptionalBoolFalse
default :
isPull = util . OptionalBoolNone
}
// FIXME: we should be more efficient here
createdByID := getUserIDForFilter ( ctx , "created_by" )
if ctx . Written ( ) {
return
}
assignedByID := getUserIDForFilter ( ctx , "assigned_by" )
if ctx . Written ( ) {
return
}
mentionedByID := getUserIDForFilter ( ctx , "mentioned_by" )
if ctx . Written ( ) {
return
}
// Only fetch the issues if we either don't have a keyword or the search returned issues
// This would otherwise return all issues if no issues were found by the search.
if len ( keyword ) == 0 || len ( issueIDs ) > 0 || len ( labelIDs ) > 0 {
2022-06-13 12:37:59 +03:00
issuesOpt := & issues_model . IssuesOptions {
2022-04-07 21:59:56 +03:00
ListOptions : listOptions ,
2022-04-25 17:06:24 +03:00
RepoID : ctx . Repo . Repository . ID ,
2022-04-07 21:59:56 +03:00
IsClosed : isClosed ,
IssueIDs : issueIDs ,
LabelIDs : labelIDs ,
MilestoneIDs : mileIDs ,
2023-01-29 06:45:29 +03:00
ProjectID : projectID ,
2022-04-07 21:59:56 +03:00
IsPull : isPull ,
UpdatedBeforeUnix : before ,
UpdatedAfterUnix : since ,
PosterID : createdByID ,
AssigneeID : assignedByID ,
MentionedID : mentionedByID ,
}
2022-11-19 11:12:33 +03:00
if issues , err = issues_model . Issues ( ctx , issuesOpt ) ; err != nil {
2022-04-07 21:59:56 +03:00
ctx . Error ( http . StatusInternalServerError , err . Error ( ) )
return
}
issuesOpt . ListOptions = db . ListOptions {
Page : - 1 ,
}
2022-11-19 11:12:33 +03:00
if filteredCount , err = issues_model . CountIssues ( ctx , issuesOpt ) ; err != nil {
2022-04-07 21:59:56 +03:00
ctx . Error ( http . StatusInternalServerError , err . Error ( ) )
return
}
}
ctx . SetTotalCountHeader ( filteredCount )
2022-11-19 11:12:33 +03:00
ctx . JSON ( http . StatusOK , convert . ToAPIIssueList ( ctx , issues ) )
2022-04-07 21:59:56 +03:00
}
2017-03-15 04:10:35 +03:00
// UpdateIssueStatus change issue's status
func UpdateIssueStatus ( ctx * context . Context ) {
issues := getActionIssues ( ctx )
if ctx . Written ( ) {
2014-07-26 10:28:04 +04:00
return
}
2017-03-15 04:10:35 +03:00
var isClosed bool
2021-08-11 03:31:13 +03:00
switch action := ctx . FormString ( "action" ) ; action {
2017-03-15 04:10:35 +03:00
case "open" :
isClosed = false
case "close" :
isClosed = true
default :
log . Warn ( "Unrecognized action: %s" , action )
}
2022-11-19 11:12:33 +03:00
if _ , err := issues_model . IssueList ( issues ) . LoadRepositories ( ctx ) ; err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "LoadRepositories" , err )
2017-03-15 04:10:35 +03:00
return
}
for _ , issue := range issues {
2018-10-18 14:23:05 +03:00
if issue . IsClosed != isClosed {
2023-01-25 07:47:53 +03:00
if err := issue_service . ChangeStatus ( issue , ctx . Doer , "" , isClosed ) ; err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrDependenciesLeft ( err ) {
2018-10-18 14:23:05 +03:00
ctx . JSON ( http . StatusPreconditionFailed , map [ string ] interface { } {
2023-04-26 19:54:17 +03:00
"error" : ctx . Tr ( "repo.issues.dependency.issue_batch_close_blocked" , issue . Index ) ,
2018-10-18 14:23:05 +03:00
} )
return
}
ctx . ServerError ( "ChangeStatus" , err )
2018-07-18 00:23:58 +03:00
return
}
2017-03-15 04:10:35 +03:00
}
}
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , map [ string ] interface { } {
2014-07-26 10:28:04 +04:00
"ok" : true ,
} )
}
2016-11-24 10:04:31 +03:00
// NewComment create a comment for issue
2021-01-26 18:36:53 +03:00
func NewComment ( ctx * context . Context ) {
2021-04-06 22:44:05 +03:00
form := web . GetForm ( ctx ) . ( * forms . CreateCommentForm )
2017-10-16 10:55:43 +03:00
issue := GetActionIssue ( ctx )
if ctx . Written ( ) {
2014-07-26 10:28:04 +04:00
return
}
2022-03-22 10:03:22 +03:00
if ! ctx . IsSigned || ( ctx . Doer . ID != issue . PosterID && ! ctx . Repo . CanReadIssuesOrPulls ( issue . IsPull ) ) {
2019-04-22 23:40:51 +03:00
if log . IsTrace ( ) {
if ctx . IsSigned {
issueType := "issues"
if issue . IsPull {
issueType = "pulls"
}
log . Trace ( "Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n" +
"User in Repo has Permissions: %-+v" ,
2022-03-22 10:03:22 +03:00
ctx . Doer ,
2019-04-22 23:40:51 +03:00
log . NewColoredIDValue ( issue . PosterID ) ,
issueType ,
ctx . Repo . Repository ,
ctx . Repo . Permission )
} else {
log . Trace ( "Permission Denied: Not logged in" )
}
}
2021-04-05 18:30:52 +03:00
ctx . Error ( http . StatusForbidden )
2020-01-20 15:00:32 +03:00
return
2019-02-18 23:55:04 +03:00
}
2022-03-22 10:03:22 +03:00
if issue . IsLocked && ! ctx . Repo . CanWriteIssuesOrPulls ( issue . IsPull ) && ! ctx . Doer . IsAdmin {
2019-02-18 23:55:04 +03:00
ctx . Flash . Error ( ctx . Tr ( "repo.issues.comment_on_locked" ) )
2023-02-11 09:34:11 +03:00
ctx . Redirect ( issue . Link ( ) )
2018-11-28 14:26:14 +03:00
return
}
2015-08-13 11:07:11 +03:00
var attachments [ ] string
2020-08-18 07:23:45 +03:00
if setting . Attachment . Enabled {
2016-08-11 15:48:08 +03:00
attachments = form . Files
2014-07-26 10:28:04 +04:00
}
2015-08-13 11:07:11 +03:00
if ctx . HasError ( ) {
ctx . Flash . Error ( ctx . Data [ "ErrorMsg" ] . ( string ) )
2023-02-11 09:34:11 +03:00
ctx . Redirect ( issue . Link ( ) )
2015-08-13 11:07:11 +03:00
return
2014-07-26 10:28:04 +04:00
}
2022-06-13 12:37:59 +03:00
var comment * issues_model . Comment
2015-09-13 18:26:25 +03:00
defer func ( ) {
2015-11-01 01:59:07 +03:00
// Check if issue admin/poster changes the status of issue.
2022-03-22 10:03:22 +03:00
if ( ctx . Repo . CanWriteIssuesOrPulls ( issue . IsPull ) || ( ctx . IsSigned && issue . IsPoster ( ctx . Doer . ID ) ) ) &&
2015-09-13 18:26:25 +03:00
( form . Status == "reopen" || form . Status == "close" ) &&
2016-08-16 20:19:09 +03:00
! ( issue . IsPull && issue . PullRequest . HasMerged ) {
2015-10-19 02:30:39 +03:00
2015-10-25 10:10:22 +03:00
// Duplication and conflict check should apply to reopen pull request.
2022-06-13 12:37:59 +03:00
var pr * issues_model . PullRequest
2015-10-19 02:30:39 +03:00
2015-10-23 17:31:13 +03:00
if form . Status == "reopen" && issue . IsPull {
2015-10-19 02:30:39 +03:00
pull := issue . PullRequest
2019-06-12 22:41:28 +03:00
var err error
2022-11-19 11:12:33 +03:00
pr , err = issues_model . GetUnmergedPullRequest ( ctx , pull . HeadRepoID , pull . BaseRepoID , pull . HeadBranch , pull . BaseBranch , pull . Flow )
2015-10-19 02:30:39 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if ! issues_model . IsErrPullRequestNotExist ( err ) {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "GetUnmergedPullRequest" , err )
2015-10-19 02:30:39 +03:00
return
}
}
2015-10-25 10:10:22 +03:00
// Regenerate patch and test conflict.
if pr == nil {
2021-07-28 12:42:56 +03:00
issue . PullRequest . HeadCommitID = ""
2019-12-07 05:44:10 +03:00
pull_service . AddToTaskQueue ( issue . PullRequest )
2015-10-25 10:10:22 +03:00
}
2015-10-19 02:30:39 +03:00
}
if pr != nil {
ctx . Flash . Info ( ctx . Tr ( "repo.pulls.open_unmerged_pull_exists" , pr . Index ) )
2015-09-13 18:26:25 +03:00
} else {
2018-10-18 14:23:05 +03:00
isClosed := form . Status == "close"
2023-01-25 07:47:53 +03:00
if err := issue_service . ChangeStatus ( issue , ctx . Doer , "" , isClosed ) ; err != nil {
2019-04-02 10:48:31 +03:00
log . Error ( "ChangeStatus: %v" , err )
2018-07-18 00:23:58 +03:00
2022-06-13 12:37:59 +03:00
if issues_model . IsErrDependenciesLeft ( err ) {
2018-07-18 00:23:58 +03:00
if issue . IsPull {
ctx . Flash . Error ( ctx . Tr ( "repo.issues.dependency.pr_close_blocked" ) )
2022-03-23 07:54:07 +03:00
ctx . Redirect ( fmt . Sprintf ( "%s/pulls/%d" , ctx . Repo . RepoLink , issue . Index ) )
2018-07-18 00:23:58 +03:00
} else {
ctx . Flash . Error ( ctx . Tr ( "repo.issues.dependency.issue_close_blocked" ) )
2022-03-23 07:54:07 +03:00
ctx . Redirect ( fmt . Sprintf ( "%s/issues/%d" , ctx . Repo . RepoLink , issue . Index ) )
2018-07-18 00:23:58 +03:00
}
return
}
2015-10-19 02:30:39 +03:00
} else {
2022-03-22 10:03:22 +03:00
if err := stopTimerIfAvailable ( ctx . Doer , issue ) ; err != nil {
2019-02-05 14:38:11 +03:00
ctx . ServerError ( "CreateOrStopIssueStopwatch" , err )
return
}
2016-02-22 20:40:00 +03:00
log . Trace ( "Issue [%d] status changed to closed: %v" , issue . ID , issue . IsClosed )
2015-10-19 02:30:39 +03:00
}
2015-09-13 18:26:25 +03:00
}
}
2015-10-19 02:30:39 +03:00
// Redirect to comment hashtag if there is any actual content.
typeName := "issues"
if issue . IsPull {
typeName = "pulls"
}
if comment != nil {
ctx . Redirect ( fmt . Sprintf ( "%s/%s/%d#%s" , ctx . Repo . RepoLink , typeName , issue . Index , comment . HashTag ( ) ) )
} else {
ctx . Redirect ( fmt . Sprintf ( "%s/%s/%d" , ctx . Repo . RepoLink , typeName , issue . Index ) )
}
2015-09-13 18:26:25 +03:00
} ( )
2015-08-13 11:07:11 +03:00
// Fix #321: Allow empty comments, as long as we have attachments.
if len ( form . Content ) == 0 && len ( attachments ) == 0 {
return
2014-07-26 10:28:04 +04:00
}
2022-12-10 05:46:31 +03:00
comment , err := issue_service . CreateIssueComment ( ctx , ctx . Doer , ctx . Repo . Repository , issue , form . Content , attachments )
2015-08-13 11:07:11 +03:00
if err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "CreateIssueComment" , err )
2014-07-26 10:28:04 +04:00
return
}
2015-08-13 11:07:11 +03:00
log . Trace ( "Comment created: %d/%d/%d" , ctx . Repo . Repository . ID , issue . ID , comment . ID )
2014-07-26 10:28:04 +04:00
}
2016-11-24 10:04:31 +03:00
// UpdateCommentContent change comment of issue's content
2016-03-11 19:56:52 +03:00
func UpdateCommentContent ( ctx * context . Context ) {
2022-06-13 12:37:59 +03:00
comment , err := issues_model . GetCommentByID ( ctx , ctx . ParamsInt64 ( ":id" ) )
2015-08-19 23:31:28 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
ctx . NotFoundOrServerError ( "GetCommentByID" , issues_model . IsErrCommentNotExist , err )
2015-08-19 23:31:28 +03:00
return
}
2022-11-19 11:12:33 +03:00
if err := comment . LoadIssue ( ctx ) ; err != nil {
2022-06-13 12:37:59 +03:00
ctx . NotFoundOrServerError ( "LoadIssue" , issues_model . IsErrIssueNotExist , err )
2018-11-28 14:26:14 +03:00
return
}
2022-03-22 10:03:22 +03:00
if ! ctx . IsSigned || ( ctx . Doer . ID != comment . PosterID && ! ctx . Repo . CanWriteIssuesOrPulls ( comment . Issue . IsPull ) ) {
2021-04-05 18:30:52 +03:00
ctx . Error ( http . StatusForbidden )
2015-08-19 23:31:28 +03:00
return
2022-01-18 20:28:38 +03:00
}
2023-04-20 09:39:44 +03:00
if ! comment . Type . HasContentSupport ( ) {
2021-04-05 18:30:52 +03:00
ctx . Error ( http . StatusNoContent )
2015-08-19 23:31:28 +03:00
return
}
2018-05-16 17:01:55 +03:00
oldContent := comment . Content
2021-08-11 03:31:13 +03:00
comment . Content = ctx . FormString ( "content" )
2015-08-19 23:31:28 +03:00
if len ( comment . Content ) == 0 {
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , map [ string ] interface { } {
2015-08-19 23:31:28 +03:00
"content" : "" ,
} )
return
}
2022-12-10 05:46:31 +03:00
if err = issue_service . UpdateComment ( ctx , comment , ctx . Doer , oldContent ) ; err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "UpdateComment" , err )
2015-08-19 23:31:28 +03:00
return
}
2022-11-19 11:12:33 +03:00
if err := comment . LoadAttachments ( ctx ) ; err != nil {
2022-01-18 20:28:38 +03:00
ctx . ServerError ( "LoadAttachments" , err )
return
2021-08-20 22:26:19 +03:00
}
2021-08-21 16:04:47 +03:00
// when the update request doesn't intend to update attachments (eg: change checkbox state), ignore attachment updates
if ! ctx . FormBool ( "ignore_attachments" ) {
2022-05-20 17:08:52 +03:00
if err := updateAttachments ( ctx , comment , ctx . FormStrings ( "files[]" ) ) ; err != nil {
2021-08-21 16:04:47 +03:00
ctx . ServerError ( "UpdateAttachments" , err )
return
}
2021-04-20 01:25:08 +03:00
}
content , err := markdown . RenderString ( & markup . RenderContext {
2021-11-16 21:18:25 +03:00
URLPrefix : ctx . FormString ( "context" ) , // FIXME: <- IS THIS SAFE ?
2021-04-20 01:25:08 +03:00
Metas : ctx . Repo . Repository . ComposeMetas ( ) ,
2021-06-21 01:39:12 +03:00
GitRepo : ctx . Repo . GitRepo ,
2021-08-28 23:15:56 +03:00
Ctx : ctx ,
2021-04-20 01:25:08 +03:00
} , comment . Content )
if err != nil {
ctx . ServerError ( "RenderString" , err )
return
2019-10-15 15:19:32 +03:00
}
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , map [ string ] interface { } {
2021-04-20 01:25:08 +03:00
"content" : content ,
2020-12-13 22:12:27 +03:00
"attachments" : attachmentsHTML ( ctx , comment . Attachments , comment . Content ) ,
2015-08-19 23:31:28 +03:00
} )
}
2016-11-24 10:04:31 +03:00
// DeleteComment delete comment of issue
2016-07-25 21:48:17 +03:00
func DeleteComment ( ctx * context . Context ) {
2022-06-13 12:37:59 +03:00
comment , err := issues_model . GetCommentByID ( ctx , ctx . ParamsInt64 ( ":id" ) )
2016-07-25 21:48:17 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
ctx . NotFoundOrServerError ( "GetCommentByID" , issues_model . IsErrCommentNotExist , err )
2016-07-25 21:48:17 +03:00
return
}
2022-11-19 11:12:33 +03:00
if err := comment . LoadIssue ( ctx ) ; err != nil {
2022-06-13 12:37:59 +03:00
ctx . NotFoundOrServerError ( "LoadIssue" , issues_model . IsErrIssueNotExist , err )
2018-11-28 14:26:14 +03:00
return
}
2022-03-22 10:03:22 +03:00
if ! ctx . IsSigned || ( ctx . Doer . ID != comment . PosterID && ! ctx . Repo . CanWriteIssuesOrPulls ( comment . Issue . IsPull ) ) {
2021-04-05 18:30:52 +03:00
ctx . Error ( http . StatusForbidden )
2016-07-25 21:48:17 +03:00
return
2023-04-20 09:39:44 +03:00
} else if ! comment . Type . HasContentSupport ( ) {
2021-04-05 18:30:52 +03:00
ctx . Error ( http . StatusNoContent )
2016-07-25 21:48:17 +03:00
return
}
2022-12-10 05:46:31 +03:00
if err = issue_service . DeleteComment ( ctx , ctx . Doer , comment ) ; err != nil {
2022-11-19 11:12:33 +03:00
ctx . ServerError ( "DeleteComment" , err )
2016-07-25 21:48:17 +03:00
return
}
2022-03-23 07:54:07 +03:00
ctx . Status ( http . StatusOK )
2016-07-25 21:48:17 +03:00
}
2017-12-04 02:14:26 +03:00
// ChangeIssueReaction create a reaction for issue
2021-01-26 18:36:53 +03:00
func ChangeIssueReaction ( ctx * context . Context ) {
2021-04-06 22:44:05 +03:00
form := web . GetForm ( ctx ) . ( * forms . ReactionForm )
2017-12-04 02:14:26 +03:00
issue := GetActionIssue ( ctx )
if ctx . Written ( ) {
return
}
2022-03-22 10:03:22 +03:00
if ! ctx . IsSigned || ( ctx . Doer . ID != issue . PosterID && ! ctx . Repo . CanReadIssuesOrPulls ( issue . IsPull ) ) {
2019-04-22 23:40:51 +03:00
if log . IsTrace ( ) {
if ctx . IsSigned {
issueType := "issues"
if issue . IsPull {
issueType = "pulls"
}
log . Trace ( "Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n" +
"User in Repo has Permissions: %-+v" ,
2022-03-22 10:03:22 +03:00
ctx . Doer ,
2019-04-22 23:40:51 +03:00
log . NewColoredIDValue ( issue . PosterID ) ,
issueType ,
ctx . Repo . Repository ,
ctx . Repo . Permission )
} else {
log . Trace ( "Permission Denied: Not logged in" )
}
}
2021-04-05 18:30:52 +03:00
ctx . Error ( http . StatusForbidden )
2018-11-28 14:26:14 +03:00
return
}
2017-12-04 02:14:26 +03:00
if ctx . HasError ( ) {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "ChangeIssueReaction" , errors . New ( ctx . GetErrMsg ( ) ) )
2017-12-04 02:14:26 +03:00
return
}
switch ctx . Params ( ":action" ) {
case "react" :
2022-03-31 12:20:39 +03:00
reaction , err := issues_model . CreateIssueReaction ( ctx . Doer . ID , issue . ID , form . Content )
2017-12-04 02:14:26 +03:00
if err != nil {
2022-03-31 12:20:39 +03:00
if issues_model . IsErrForbiddenIssueReaction ( err ) {
2019-12-08 01:04:19 +03:00
ctx . ServerError ( "ChangeIssueReaction" , err )
return
}
2017-12-04 02:14:26 +03:00
log . Info ( "CreateIssueReaction: %s" , err )
break
}
// Reload new reactions
issue . Reactions = nil
2022-06-13 12:37:59 +03:00
if err = issue . LoadAttributes ( ctx ) ; err != nil {
2017-12-04 02:14:26 +03:00
log . Info ( "issue.LoadAttributes: %s" , err )
break
}
log . Trace ( "Reaction for issue created: %d/%d/%d" , ctx . Repo . Repository . ID , issue . ID , reaction . ID )
case "unreact" :
2022-03-31 12:20:39 +03:00
if err := issues_model . DeleteIssueReaction ( ctx . Doer . ID , issue . ID , form . Content ) ; err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "DeleteIssueReaction" , err )
2017-12-04 02:14:26 +03:00
return
}
// Reload new reactions
issue . Reactions = nil
2022-06-13 12:37:59 +03:00
if err := issue . LoadAttributes ( ctx ) ; err != nil {
2017-12-04 02:14:26 +03:00
log . Info ( "issue.LoadAttributes: %s" , err )
break
}
log . Trace ( "Reaction for issue removed: %d/%d" , ctx . Repo . Repository . ID , issue . ID )
default :
2018-01-11 00:34:17 +03:00
ctx . NotFound ( fmt . Sprintf ( "Unknown action %s" , ctx . Params ( ":action" ) ) , nil )
2017-12-04 02:14:26 +03:00
return
}
if len ( issue . Reactions ) == 0 {
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , map [ string ] interface { } {
2017-12-04 02:14:26 +03:00
"empty" : true ,
"html" : "" ,
} )
return
}
2021-12-15 09:59:57 +03:00
html , err := ctx . RenderToString ( tplReactions , map [ string ] interface { } {
2023-03-02 20:44:06 +03:00
"ctxData" : ctx . Data ,
2017-12-04 02:14:26 +03:00
"ActionURL" : fmt . Sprintf ( "%s/issues/%d/reactions" , ctx . Repo . RepoLink , issue . Index ) ,
"Reactions" : issue . Reactions . GroupByType ( ) ,
} )
if err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "ChangeIssueReaction.HTMLString" , err )
2017-12-04 02:14:26 +03:00
return
}
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , map [ string ] interface { } {
2017-12-04 02:14:26 +03:00
"html" : html ,
} )
}
// ChangeCommentReaction create a reaction for comment
2021-01-26 18:36:53 +03:00
func ChangeCommentReaction ( ctx * context . Context ) {
2021-04-06 22:44:05 +03:00
form := web . GetForm ( ctx ) . ( * forms . ReactionForm )
2022-06-13 12:37:59 +03:00
comment , err := issues_model . GetCommentByID ( ctx , ctx . ParamsInt64 ( ":id" ) )
2017-12-04 02:14:26 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
ctx . NotFoundOrServerError ( "GetCommentByID" , issues_model . IsErrCommentNotExist , err )
2017-12-04 02:14:26 +03:00
return
}
2022-11-19 11:12:33 +03:00
if err := comment . LoadIssue ( ctx ) ; err != nil {
2022-06-13 12:37:59 +03:00
ctx . NotFoundOrServerError ( "LoadIssue" , issues_model . IsErrIssueNotExist , err )
2017-12-04 02:14:26 +03:00
return
}
2022-03-22 10:03:22 +03:00
if ! ctx . IsSigned || ( ctx . Doer . ID != comment . PosterID && ! ctx . Repo . CanReadIssuesOrPulls ( comment . Issue . IsPull ) ) {
2019-04-22 23:40:51 +03:00
if log . IsTrace ( ) {
if ctx . IsSigned {
issueType := "issues"
if comment . Issue . IsPull {
issueType = "pulls"
}
log . Trace ( "Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n" +
"User in Repo has Permissions: %-+v" ,
2022-03-22 10:03:22 +03:00
ctx . Doer ,
2019-04-22 23:40:51 +03:00
log . NewColoredIDValue ( comment . Issue . PosterID ) ,
issueType ,
ctx . Repo . Repository ,
ctx . Repo . Permission )
} else {
log . Trace ( "Permission Denied: Not logged in" )
}
}
2021-04-05 18:30:52 +03:00
ctx . Error ( http . StatusForbidden )
2018-11-28 14:26:14 +03:00
return
2022-01-18 20:28:38 +03:00
}
2023-04-20 09:39:44 +03:00
if ! comment . Type . HasContentSupport ( ) {
2021-04-05 18:30:52 +03:00
ctx . Error ( http . StatusNoContent )
2017-12-04 02:14:26 +03:00
return
}
switch ctx . Params ( ":action" ) {
case "react" :
2022-03-31 12:20:39 +03:00
reaction , err := issues_model . CreateCommentReaction ( ctx . Doer . ID , comment . Issue . ID , comment . ID , form . Content )
2017-12-04 02:14:26 +03:00
if err != nil {
2022-03-31 12:20:39 +03:00
if issues_model . IsErrForbiddenIssueReaction ( err ) {
2019-12-08 01:04:19 +03:00
ctx . ServerError ( "ChangeIssueReaction" , err )
return
}
2017-12-04 02:14:26 +03:00
log . Info ( "CreateCommentReaction: %s" , err )
break
}
// Reload new reactions
comment . Reactions = nil
2020-01-15 14:14:07 +03:00
if err = comment . LoadReactions ( ctx . Repo . Repository ) ; err != nil {
2017-12-04 02:14:26 +03:00
log . Info ( "comment.LoadReactions: %s" , err )
break
}
2018-11-28 14:26:14 +03:00
log . Trace ( "Reaction for comment created: %d/%d/%d/%d" , ctx . Repo . Repository . ID , comment . Issue . ID , comment . ID , reaction . ID )
2017-12-04 02:14:26 +03:00
case "unreact" :
2022-03-31 12:20:39 +03:00
if err := issues_model . DeleteCommentReaction ( ctx . Doer . ID , comment . Issue . ID , comment . ID , form . Content ) ; err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "DeleteCommentReaction" , err )
2017-12-04 02:14:26 +03:00
return
}
// Reload new reactions
comment . Reactions = nil
2020-01-15 14:14:07 +03:00
if err = comment . LoadReactions ( ctx . Repo . Repository ) ; err != nil {
2017-12-04 02:14:26 +03:00
log . Info ( "comment.LoadReactions: %s" , err )
break
}
2018-11-28 14:26:14 +03:00
log . Trace ( "Reaction for comment removed: %d/%d/%d" , ctx . Repo . Repository . ID , comment . Issue . ID , comment . ID )
2017-12-04 02:14:26 +03:00
default :
2018-01-11 00:34:17 +03:00
ctx . NotFound ( fmt . Sprintf ( "Unknown action %s" , ctx . Params ( ":action" ) ) , nil )
2017-12-04 02:14:26 +03:00
return
}
if len ( comment . Reactions ) == 0 {
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , map [ string ] interface { } {
2017-12-04 02:14:26 +03:00
"empty" : true ,
"html" : "" ,
} )
return
}
2021-12-15 09:59:57 +03:00
html , err := ctx . RenderToString ( tplReactions , map [ string ] interface { } {
2023-03-02 20:44:06 +03:00
"ctxData" : ctx . Data ,
2017-12-04 02:14:26 +03:00
"ActionURL" : fmt . Sprintf ( "%s/comments/%d/reactions" , ctx . Repo . RepoLink , comment . ID ) ,
"Reactions" : comment . Reactions . GroupByType ( ) ,
} )
if err != nil {
2018-01-11 00:34:17 +03:00
ctx . ServerError ( "ChangeCommentReaction.HTMLString" , err )
2017-12-04 02:14:26 +03:00
return
}
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , map [ string ] interface { } {
2017-12-04 02:14:26 +03:00
"html" : html ,
} )
}
2019-09-07 17:53:35 +03:00
2021-11-24 12:49:20 +03:00
func addParticipant ( poster * user_model . User , participants [ ] * user_model . User ) [ ] * user_model . User {
2019-09-07 17:53:35 +03:00
for _ , part := range participants {
if poster . ID == part . ID {
return participants
}
}
return append ( participants , poster )
}
2019-09-20 08:45:38 +03:00
2022-06-13 12:37:59 +03:00
func filterXRefComments ( ctx * context . Context , issue * issues_model . Issue ) error {
2019-09-20 08:45:38 +03:00
// Remove comments that the user has no permissions to see
for i := 0 ; i < len ( issue . Comments ) ; {
c := issue . Comments [ i ]
2022-06-13 12:37:59 +03:00
if issues_model . CommentTypeIsRef ( c . Type ) && c . RefRepoID != issue . RepoID && c . RefRepoID != 0 {
2019-09-20 08:45:38 +03:00
var err error
// Set RefRepo for description in template
2022-12-03 05:48:26 +03:00
c . RefRepo , err = repo_model . GetRepositoryByID ( ctx , c . RefRepoID )
2019-09-20 08:45:38 +03:00
if err != nil {
return err
}
2022-05-11 13:09:36 +03:00
perm , err := access_model . GetUserRepoPermission ( ctx , c . RefRepo , ctx . Doer )
2019-09-20 08:45:38 +03:00
if err != nil {
return err
}
if ! perm . CanReadIssuesOrPulls ( c . RefIsPull ) {
issue . Comments = append ( issue . Comments [ : i ] , issue . Comments [ i + 1 : ] ... )
continue
}
}
i ++
}
return nil
}
2019-10-15 15:19:32 +03:00
// GetIssueAttachments returns attachments for the issue
func GetIssueAttachments ( ctx * context . Context ) {
issue := GetActionIssue ( ctx )
2022-01-20 20:46:10 +03:00
attachments := make ( [ ] * api . Attachment , len ( issue . Attachments ) )
2019-10-15 15:19:32 +03:00
for i := 0 ; i < len ( issue . Attachments ) ; i ++ {
2022-12-09 09:35:56 +03:00
attachments [ i ] = convert . ToAttachment ( issue . Attachments [ i ] )
2019-10-15 15:19:32 +03:00
}
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , attachments )
2019-10-15 15:19:32 +03:00
}
// GetCommentAttachments returns attachments for the comment
func GetCommentAttachments ( ctx * context . Context ) {
2022-06-13 12:37:59 +03:00
comment , err := issues_model . GetCommentByID ( ctx , ctx . ParamsInt64 ( ":id" ) )
2019-10-15 15:19:32 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
ctx . NotFoundOrServerError ( "GetCommentByID" , issues_model . IsErrCommentNotExist , err )
2019-10-15 15:19:32 +03:00
return
}
2023-04-20 09:39:44 +03:00
if ! comment . Type . HasAttachmentSupport ( ) {
ctx . ServerError ( "GetCommentAttachments" , fmt . Errorf ( "comment type %v does not support attachments" , comment . Type ) )
return
}
2022-01-20 20:46:10 +03:00
attachments := make ( [ ] * api . Attachment , 0 )
2023-04-20 09:39:44 +03:00
if err := comment . LoadAttachments ( ctx ) ; err != nil {
ctx . ServerError ( "LoadAttachments" , err )
return
}
for i := 0 ; i < len ( comment . Attachments ) ; i ++ {
attachments = append ( attachments , convert . ToAttachment ( comment . Attachments [ i ] ) )
2019-10-15 15:19:32 +03:00
}
2021-04-05 18:30:52 +03:00
ctx . JSON ( http . StatusOK , attachments )
2019-10-15 15:19:32 +03:00
}
2022-05-20 17:08:52 +03:00
func updateAttachments ( ctx * context . Context , item interface { } , files [ ] string ) error {
2021-11-19 16:39:57 +03:00
var attachments [ ] * repo_model . Attachment
2019-10-15 15:19:32 +03:00
switch content := item . ( type ) {
2022-06-13 12:37:59 +03:00
case * issues_model . Issue :
2019-10-15 15:19:32 +03:00
attachments = content . Attachments
2022-06-13 12:37:59 +03:00
case * issues_model . Comment :
2019-10-15 15:19:32 +03:00
attachments = content . Attachments
default :
2022-02-26 15:15:32 +03:00
return fmt . Errorf ( "unknown Type: %T" , content )
2019-10-15 15:19:32 +03:00
}
for i := 0 ; i < len ( attachments ) ; i ++ {
Improve utils of slices (#22379)
- Move the file `compare.go` and `slice.go` to `slice.go`.
- Fix `ExistsInSlice`, it's buggy
- It uses `sort.Search`, so it assumes that the input slice is sorted.
- It passes `func(i int) bool { return slice[i] == target })` to
`sort.Search`, that's incorrect, check the doc of `sort.Search`.
- Conbine `IsInt64InSlice(int64, []int64)` and `ExistsInSlice(string,
[]string)` to `SliceContains[T]([]T, T)`.
- Conbine `IsSliceInt64Eq([]int64, []int64)` and `IsEqualSlice([]string,
[]string)` to `SliceSortedEqual[T]([]T, T)`.
- Add `SliceEqual[T]([]T, T)` as a distinction from
`SliceSortedEqual[T]([]T, T)`.
- Redesign `RemoveIDFromList([]int64, int64) ([]int64, bool)` to
`SliceRemoveAll[T]([]T, T) []T`.
- Add `SliceContainsFunc[T]([]T, func(T) bool)` and
`SliceRemoveAllFunc[T]([]T, func(T) bool)` for general use.
- Add comments to explain why not `golang.org/x/exp/slices`.
- Add unit tests.
2023-01-11 08:31:16 +03:00
if util . SliceContainsString ( files , attachments [ i ] . UUID ) {
2019-10-15 15:19:32 +03:00
continue
}
2021-11-19 16:39:57 +03:00
if err := repo_model . DeleteAttachment ( attachments [ i ] , true ) ; err != nil {
2019-10-15 15:19:32 +03:00
return err
}
}
var err error
if len ( files ) > 0 {
switch content := item . ( type ) {
2022-06-13 12:37:59 +03:00
case * issues_model . Issue :
err = issues_model . UpdateIssueAttachments ( content . ID , files )
case * issues_model . Comment :
2019-10-15 15:19:32 +03:00
err = content . UpdateAttachments ( files )
default :
2022-02-26 15:15:32 +03:00
return fmt . Errorf ( "unknown Type: %T" , content )
2019-10-15 15:19:32 +03:00
}
if err != nil {
return err
}
}
switch content := item . ( type ) {
2022-06-13 12:37:59 +03:00
case * issues_model . Issue :
2022-05-20 17:08:52 +03:00
content . Attachments , err = repo_model . GetAttachmentsByIssueID ( ctx , content . ID )
2022-06-13 12:37:59 +03:00
case * issues_model . Comment :
2022-05-20 17:08:52 +03:00
content . Attachments , err = repo_model . GetAttachmentsByCommentID ( ctx , content . ID )
2019-10-15 15:19:32 +03:00
default :
2022-02-26 15:15:32 +03:00
return fmt . Errorf ( "unknown Type: %T" , content )
2019-10-15 15:19:32 +03:00
}
return err
}
2021-11-19 16:39:57 +03:00
func attachmentsHTML ( ctx * context . Context , attachments [ ] * repo_model . Attachment , content string ) string {
2021-12-15 09:59:57 +03:00
attachHTML , err := ctx . RenderToString ( tplAttachment , map [ string ] interface { } {
2023-03-02 20:44:06 +03:00
"ctxData" : ctx . Data ,
2019-10-15 15:19:32 +03:00
"Attachments" : attachments ,
2020-12-13 22:12:27 +03:00
"Content" : content ,
2019-10-15 15:19:32 +03:00
} )
if err != nil {
ctx . ServerError ( "attachmentsHTML.HTMLString" , err )
return ""
}
return attachHTML
}
2020-10-26 00:49:48 +03:00
2021-03-05 18:17:32 +03:00
// combineLabelComments combine the nearby label comments as one.
2022-06-13 12:37:59 +03:00
func combineLabelComments ( issue * issues_model . Issue ) {
var prev , cur * issues_model . Comment
2020-11-21 01:29:09 +03:00
for i := 0 ; i < len ( issue . Comments ) ; i ++ {
2021-03-05 18:17:32 +03:00
cur = issue . Comments [ i ]
2020-11-21 01:29:09 +03:00
if i > 0 {
2020-10-26 00:49:48 +03:00
prev = issue . Comments [ i - 1 ]
}
2022-06-13 12:37:59 +03:00
if i == 0 || cur . Type != issues_model . CommentTypeLabel ||
2020-11-21 01:29:09 +03:00
( prev != nil && prev . PosterID != cur . PosterID ) ||
( prev != nil && cur . CreatedUnix - prev . CreatedUnix >= 60 ) {
2022-06-13 12:37:59 +03:00
if cur . Type == issues_model . CommentTypeLabel && cur . Label != nil {
2020-11-21 01:29:09 +03:00
if cur . Content != "1" {
cur . RemovedLabels = append ( cur . RemovedLabels , cur . Label )
2020-10-26 00:49:48 +03:00
} else {
2020-11-21 01:29:09 +03:00
cur . AddedLabels = append ( cur . AddedLabels , cur . Label )
2020-10-26 00:49:48 +03:00
}
}
2020-11-21 01:29:09 +03:00
continue
2020-10-26 00:49:48 +03:00
}
2020-11-21 01:29:09 +03:00
2021-03-05 18:17:32 +03:00
if cur . Label != nil { // now cur MUST be label comment
2022-06-13 12:37:59 +03:00
if prev . Type == issues_model . CommentTypeLabel { // we can combine them only prev is a label comment
2021-03-05 18:17:32 +03:00
if cur . Content != "1" {
2021-11-04 17:51:30 +03:00
// 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 )
}
2021-03-05 18:17:32 +03:00
} else {
2021-11-04 17:51:30 +03:00
// 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 )
}
2021-03-05 18:17:32 +03:00
}
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 )
}
2021-02-10 05:50:44 +03:00
}
2020-11-21 01:29:09 +03:00
}
2020-10-26 00:49:48 +03:00
}
}
2020-12-21 18:39:28 +03:00
// get all teams that current user can mention
func handleTeamMentions ( ctx * context . Context ) {
2022-03-22 10:03:22 +03:00
if ctx . Doer == nil || ! ctx . Repo . Owner . IsOrganization ( ) {
2020-12-21 18:39:28 +03:00
return
}
2021-11-19 14:41:40 +03:00
var isAdmin bool
2020-12-21 18:39:28 +03:00
var err error
2022-03-29 09:29:02 +03:00
var teams [ ] * organization . Team
org := organization . OrgFromUser ( ctx . Repo . Owner )
2020-12-21 18:39:28 +03:00
// Admin has super access.
2022-03-22 10:03:22 +03:00
if ctx . Doer . IsAdmin {
2020-12-21 18:39:28 +03:00
isAdmin = true
} else {
2022-03-22 10:03:22 +03:00
isAdmin , err = org . IsOwnedBy ( ctx . Doer . ID )
2020-12-21 18:39:28 +03:00
if err != nil {
ctx . ServerError ( "IsOwnedBy" , err )
return
}
}
if isAdmin {
2021-11-19 14:41:40 +03:00
teams , err = org . LoadTeams ( )
if err != nil {
2021-08-12 15:43:08 +03:00
ctx . ServerError ( "LoadTeams" , err )
2020-12-21 18:39:28 +03:00
return
}
} else {
2022-03-22 10:03:22 +03:00
teams , err = org . GetUserTeams ( ctx . Doer . ID )
2020-12-21 18:39:28 +03:00
if err != nil {
ctx . ServerError ( "GetUserTeams" , err )
return
}
}
2021-11-19 14:41:40 +03:00
ctx . Data [ "MentionableTeams" ] = teams
2020-12-21 18:39:28 +03:00
ctx . Data [ "MentionableTeamsOrg" ] = ctx . Repo . Owner . Name
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set
a context cache to do that. i.e. Some pages may load a user from a
database with the same id in different areas on the same page. But the
code is hidden in two different deep logic. How should we share the
user? As a result of this PR, now if both entry functions accept
`context.Context` as the first parameter and we just need to refactor
`GetUserByID` to reuse the user from the context cache. Then it will not
be loaded twice on an HTTP request.
But of course, sometimes we would like to reload an object from the
database, that's why `RemoveContextData` is also exposed.
The core context cache is here. It defines a new context
```go
type cacheContext struct {
ctx context.Context
data map[any]map[any]any
lock sync.RWMutex
}
var cacheContextKey = struct{}{}
func WithCacheContext(ctx context.Context) context.Context {
return context.WithValue(ctx, cacheContextKey, &cacheContext{
ctx: ctx,
data: make(map[any]map[any]any),
})
}
```
Then you can use the below 4 methods to read/write/del the data within
the same context.
```go
func GetContextData(ctx context.Context, tp, key any) any
func SetContextData(ctx context.Context, tp, key, value any)
func RemoveContextData(ctx context.Context, tp, key any)
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error)
```
Then let's take a look at how `system.GetString` implement it.
```go
func GetSetting(ctx context.Context, key string) (string, error) {
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
res, err := GetSettingNoCache(ctx, key)
if err != nil {
return "", err
}
return res.SettingValue, nil
})
})
}
```
First, it will check if context data include the setting object with the
key. If not, it will query from the global cache which may be memory or
a Redis cache. If not, it will get the object from the database. In the
end, if the object gets from the global cache or database, it will be
set into the context cache.
An object stored in the context cache will only be destroyed after the
context disappeared.
2023-02-15 16:37:34 +03:00
ctx . Data [ "MentionableTeamsOrgAvatar" ] = ctx . Repo . Owner . AvatarLink ( ctx )
2020-12-21 18:39:28 +03:00
}
2023-04-07 03:11:02 +03:00
type userSearchInfo struct {
UserID int64 ` json:"user_id" `
UserName string ` json:"username" `
AvatarLink string ` json:"avatar_link" `
FullName string ` json:"full_name" `
}
type userSearchResponse struct {
Results [ ] * userSearchInfo ` json:"results" `
}
// IssuePosters get posters for current repo's issues/pull requests
func IssuePosters ( ctx * context . Context ) {
repo := ctx . Repo . Repository
isPullList := ctx . Params ( ":type" ) == "pulls"
search := strings . TrimSpace ( ctx . FormString ( "q" ) )
posters , err := repo_model . GetIssuePostersWithSearch ( ctx , repo , isPullList , search , setting . UI . DefaultShowFullName )
if err != nil {
ctx . JSON ( http . StatusInternalServerError , err )
return
}
if search == "" && ctx . Doer != nil {
// the returned posters slice only contains limited number of users,
// to make the current user (doer) can quickly filter their own issues, always add doer to the posters slice
if ! util . SliceContainsFunc ( posters , func ( user * user_model . User ) bool { return user . ID == ctx . Doer . ID } ) {
posters = append ( posters , ctx . Doer )
}
}
posters = makeSelfOnTop ( ctx , posters )
resp := & userSearchResponse { }
resp . Results = make ( [ ] * userSearchInfo , len ( posters ) )
for i , user := range posters {
resp . Results [ i ] = & userSearchInfo { UserID : user . ID , UserName : user . Name , AvatarLink : user . AvatarLink ( ctx ) }
if setting . UI . DefaultShowFullName {
resp . Results [ i ] . FullName = user . FullName
}
}
ctx . JSON ( http . StatusOK , resp )
}