summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--custom/conf/app.ini.sample2
-rw-r--r--docs/content/doc/advanced/config-cheat-sheet.en-us.md1
-rw-r--r--models/issue.go49
-rw-r--r--models/issue_label.go13
-rw-r--r--models/issue_test.go9
-rw-r--r--models/pull_list.go2
-rw-r--r--modules/indexer/issues/bleve.go16
-rw-r--r--modules/indexer/issues/bleve_test.go2
-rw-r--r--modules/indexer/issues/db.go7
-rw-r--r--modules/indexer/issues/indexer.go11
-rw-r--r--modules/indexer/issues/indexer_test.go16
-rw-r--r--modules/setting/service.go2
-rw-r--r--modules/structs/issue.go8
-rw-r--r--public/css/index.css3
-rw-r--r--public/js/index.js11
-rw-r--r--public/less/_base.less12
-rw-r--r--routers/api/v1/api.go2
-rw-r--r--routers/api/v1/repo/issue.go134
-rw-r--r--routers/repo/issue.go5
-rw-r--r--routers/repo/issue_dependency.go7
-rw-r--r--templates/repo/issue/view_content/sidebar.tmpl34
-rw-r--r--templates/swagger/v1_json.tmpl73
22 files changed, 354 insertions, 65 deletions
diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample
index e6ccab95d9..33cd0506ed 100644
--- a/custom/conf/app.ini.sample
+++ b/custom/conf/app.ini.sample
@@ -479,6 +479,8 @@ DEFAULT_ORG_MEMBER_VISIBLE = false
; Default value for EnableDependencies
; Repositories will use dependencies by default depending on this setting
DEFAULT_ENABLE_DEPENDENCIES = true
+; Dependencies can be added from any repository where the user is granted access or only from the current repository depending on this setting.
+ALLOW_CROSS_REPOSITORY_DEPENDENCIES = true
; Enable heatmap on users profiles.
ENABLE_USER_HEATMAP = true
; Enable Timetracking
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index bcf871a3a4..1e24255d8d 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -297,6 +297,7 @@ relation to port exhaustion.
- `RECAPTCHA_SITEKEY`: **""**: Go to https://www.google.com/recaptcha/admin to get a sitekey for recaptcha.
- `RECAPTCHA_URL`: **https://www.google.com/recaptcha/**: Set the recaptcha url - allows the use of recaptcha net.
- `DEFAULT_ENABLE_DEPENDENCIES`: **true**: Enable this to have dependencies enabled by default.
+- `ALLOW_CROSS_REPOSITORY_DEPENDENCIES` : **true** Enable this to allow dependencies on issues from any repository where the user is granted access.
- `ENABLE_USER_HEATMAP`: **true**: Enable this to display the heatmap on users profiles.
- `EMAIL_DOMAIN_WHITELIST`: **\<empty\>**: If non-empty, list of domain names that can only be used to register
on this instance.
diff --git a/models/issue.go b/models/issue.go
index 17205cc2fa..78413468b2 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -9,6 +9,7 @@ import (
"path"
"regexp"
"sort"
+ "strconv"
"strings"
"code.gitea.io/gitea/modules/base"
@@ -378,6 +379,12 @@ func (issue *Issue) apiFormat(e Engine) *api.Issue {
Updated: issue.UpdatedUnix.AsTime(),
}
+ apiIssue.Repo = &api.RepositoryMeta{
+ ID: issue.Repo.ID,
+ Name: issue.Repo.Name,
+ FullName: issue.Repo.FullName(),
+ }
+
if issue.ClosedUnix != 0 {
apiIssue.Closed = issue.ClosedUnix.AsTimePtr()
}
@@ -1047,11 +1054,13 @@ type IssuesOptions struct {
LabelIDs []int64
SortType string
IssueIDs []int64
+ // prioritize issues from this repo
+ PriorityRepoID int64
}
// sortIssuesSession sort an issues-related session based on the provided
// sortType string
-func sortIssuesSession(sess *xorm.Session, sortType string) {
+func sortIssuesSession(sess *xorm.Session, sortType string, priorityRepoID int64) {
switch sortType {
case "oldest":
sess.Asc("issue.created_unix")
@@ -1069,6 +1078,8 @@ func sortIssuesSession(sess *xorm.Session, sortType string) {
sess.Asc("issue.deadline_unix")
case "farduedate":
sess.Desc("issue.deadline_unix")
+ case "priorityrepo":
+ sess.OrderBy("CASE WHEN issue.repo_id = " + strconv.FormatInt(priorityRepoID, 10) + " THEN 1 ELSE 2 END, issue.created_unix DESC")
default:
sess.Desc("issue.created_unix")
}
@@ -1170,7 +1181,7 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) {
defer sess.Close()
opts.setupSession(sess)
- sortIssuesSession(sess, opts.SortType)
+ sortIssuesSession(sess, opts.SortType, opts.PriorityRepoID)
issues := make([]*Issue, 0, setting.UI.IssuePagingNum)
if err := sess.Find(&issues); err != nil {
@@ -1476,8 +1487,8 @@ func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen
}
// SearchIssueIDsByKeyword search issues on database
-func SearchIssueIDsByKeyword(kw string, repoID int64, limit, start int) (int64, []int64, error) {
- var repoCond = builder.Eq{"repo_id": repoID}
+func SearchIssueIDsByKeyword(kw string, repoIDs []int64, limit, start int) (int64, []int64, error) {
+ var repoCond = builder.In("repo_id", repoIDs)
var subQuery = builder.Select("id").From("issue").Where(repoCond)
var cond = builder.And(
repoCond,
@@ -1566,33 +1577,43 @@ func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *Us
return sess.Commit()
}
+// DependencyInfo represents high level information about an issue which is a dependency of another issue.
+type DependencyInfo struct {
+ Issue `xorm:"extends"`
+ Repository `xorm:"extends"`
+}
+
// Get Blocked By Dependencies, aka all issues this issue is blocked by.
-func (issue *Issue) getBlockedByDependencies(e Engine) (issueDeps []*Issue, err error) {
+func (issue *Issue) getBlockedByDependencies(e Engine) (issueDeps []*DependencyInfo, err error) {
return issueDeps, e.
- Table("issue_dependency").
- Select("issue.*").
- Join("INNER", "issue", "issue.id = issue_dependency.dependency_id").
+ Table("issue").
+ Join("INNER", "repository", "repository.id = issue.repo_id").
+ Join("INNER", "issue_dependency", "issue_dependency.dependency_id = issue.id").
Where("issue_id = ?", issue.ID).
+ //sort by repo id then created date, with the issues of the same repo at the beginning of the list
+ OrderBy("CASE WHEN issue.repo_id = " + strconv.FormatInt(issue.RepoID, 10) + " THEN 0 ELSE issue.repo_id END, issue.created_unix DESC").
Find(&issueDeps)
}
// Get Blocking Dependencies, aka all issues this issue blocks.
-func (issue *Issue) getBlockingDependencies(e Engine) (issueDeps []*Issue, err error) {
+func (issue *Issue) getBlockingDependencies(e Engine) (issueDeps []*DependencyInfo, err error) {
return issueDeps, e.
- Table("issue_dependency").
- Select("issue.*").
- Join("INNER", "issue", "issue.id = issue_dependency.issue_id").
+ Table("issue").
+ Join("INNER", "repository", "repository.id = issue.repo_id").
+ Join("INNER", "issue_dependency", "issue_dependency.issue_id = issue.id").
Where("dependency_id = ?", issue.ID).
+ //sort by repo id then created date, with the issues of the same repo at the beginning of the list
+ OrderBy("CASE WHEN issue.repo_id = " + strconv.FormatInt(issue.RepoID, 10) + " THEN 0 ELSE issue.repo_id END, issue.created_unix DESC").
Find(&issueDeps)
}
// BlockedByDependencies finds all Dependencies an issue is blocked by
-func (issue *Issue) BlockedByDependencies() ([]*Issue, error) {
+func (issue *Issue) BlockedByDependencies() ([]*DependencyInfo, error) {
return issue.getBlockedByDependencies(x)
}
// BlockingDependencies returns all blocking dependencies, aka all other issues a given issue blocks
-func (issue *Issue) BlockingDependencies() ([]*Issue, error) {
+func (issue *Issue) BlockingDependencies() ([]*DependencyInfo, error) {
return issue.getBlockingDependencies(x)
}
diff --git a/models/issue_label.go b/models/issue_label.go
index 1fc873cfd4..497756af5b 100644
--- a/models/issue_label.go
+++ b/models/issue_label.go
@@ -250,6 +250,19 @@ func GetLabelIDsInRepoByNames(repoID int64, labelNames []string) ([]int64, error
Find(&labelIDs)
}
+// GetLabelIDsInReposByNames returns a list of labelIDs by names in one of the given
+// repositories.
+// it silently ignores label names that do not belong to the repository.
+func GetLabelIDsInReposByNames(repoIDs []int64, labelNames []string) ([]int64, error) {
+ labelIDs := make([]int64, 0, len(labelNames))
+ return labelIDs, x.Table("label").
+ In("repo_id", repoIDs).
+ In("name", labelNames).
+ Asc("name").
+ Cols("id").
+ Find(&labelIDs)
+}
+
// GetLabelInRepoByID returns a label by ID in given repository.
func GetLabelInRepoByID(repoID, labelID int64) (*Label, error) {
return getLabelInRepoByID(x, repoID, labelID)
diff --git a/models/issue_test.go b/models/issue_test.go
index 592a0e3d77..d16c1de25d 100644
--- a/models/issue_test.go
+++ b/models/issue_test.go
@@ -264,24 +264,23 @@ func TestIssue_loadTotalTimes(t *testing.T) {
func TestIssue_SearchIssueIDsByKeyword(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
-
- total, ids, err := SearchIssueIDsByKeyword("issue2", 1, 10, 0)
+ total, ids, err := SearchIssueIDsByKeyword("issue2", []int64{1}, 10, 0)
assert.NoError(t, err)
assert.EqualValues(t, 1, total)
assert.EqualValues(t, []int64{2}, ids)
- total, ids, err = SearchIssueIDsByKeyword("first", 1, 10, 0)
+ total, ids, err = SearchIssueIDsByKeyword("first", []int64{1}, 10, 0)
assert.NoError(t, err)
assert.EqualValues(t, 1, total)
assert.EqualValues(t, []int64{1}, ids)
- total, ids, err = SearchIssueIDsByKeyword("for", 1, 10, 0)
+ total, ids, err = SearchIssueIDsByKeyword("for", []int64{1}, 10, 0)
assert.NoError(t, err)
assert.EqualValues(t, 4, total)
assert.EqualValues(t, []int64{1, 2, 3, 5}, ids)
// issue1's comment id 2
- total, ids, err = SearchIssueIDsByKeyword("good", 1, 10, 0)
+ total, ids, err = SearchIssueIDsByKeyword("good", []int64{1}, 10, 0)
assert.NoError(t, err)
assert.EqualValues(t, 1, total)
assert.EqualValues(t, []int64{1}, ids)
diff --git a/models/pull_list.go b/models/pull_list.go
index 4ec6fdde3b..2c2f53f4a1 100644
--- a/models/pull_list.go
+++ b/models/pull_list.go
@@ -87,7 +87,7 @@ func PullRequests(baseRepoID int64, opts *PullRequestsOptions) ([]*PullRequest,
prs := make([]*PullRequest, 0, ItemsPerPage)
findSession, err := listPullRequestStatement(baseRepoID, opts)
- sortIssuesSession(findSession, opts.SortType)
+ sortIssuesSession(findSession, opts.SortType, 0)
if err != nil {
log.Error("listPullRequestStatement: %v", err)
return nil, maxResults, err
diff --git a/modules/indexer/issues/bleve.go b/modules/indexer/issues/bleve.go
index 36279198b8..24443e54a3 100644
--- a/modules/indexer/issues/bleve.go
+++ b/modules/indexer/issues/bleve.go
@@ -218,9 +218,18 @@ func (b *BleveIndexer) Delete(ids ...int64) error {
// Search searches for issues by given conditions.
// Returns the matching issue IDs
-func (b *BleveIndexer) Search(keyword string, repoID int64, limit, start int) (*SearchResult, error) {
+func (b *BleveIndexer) Search(keyword string, repoIDs []int64, limit, start int) (*SearchResult, error) {
+ var repoQueriesP []*query.NumericRangeQuery
+ for _, repoID := range repoIDs {
+ repoQueriesP = append(repoQueriesP, numericEqualityQuery(repoID, "RepoID"))
+ }
+ repoQueries := make([]query.Query, len(repoQueriesP))
+ for i, v := range repoQueriesP {
+ repoQueries[i] = query.Query(v)
+ }
+
indexerQuery := bleve.NewConjunctionQuery(
- numericEqualityQuery(repoID, "RepoID"),
+ bleve.NewDisjunctionQuery(repoQueries...),
bleve.NewDisjunctionQuery(
newMatchPhraseQuery(keyword, "Title", issueIndexerAnalyzer),
newMatchPhraseQuery(keyword, "Content", issueIndexerAnalyzer),
@@ -242,8 +251,7 @@ func (b *BleveIndexer) Search(keyword string, repoID int64, limit, start int) (*
return nil, err
}
ret.Hits = append(ret.Hits, Match{
- ID: id,
- RepoID: repoID,
+ ID: id,
})
}
return &ret, nil
diff --git a/modules/indexer/issues/bleve_test.go b/modules/indexer/issues/bleve_test.go
index 8ec274566f..94d935d89d 100644
--- a/modules/indexer/issues/bleve_test.go
+++ b/modules/indexer/issues/bleve_test.go
@@ -76,7 +76,7 @@ func TestBleveIndexAndSearch(t *testing.T) {
)
for _, kw := range keywords {
- res, err := indexer.Search(kw.Keyword, 2, 10, 0)
+ res, err := indexer.Search(kw.Keyword, []int64{2}, 10, 0)
assert.NoError(t, err)
var ids = make([]int64, 0, len(res.Hits))
diff --git a/modules/indexer/issues/db.go b/modules/indexer/issues/db.go
index 6e7f0c1a6e..7d4e389471 100644
--- a/modules/indexer/issues/db.go
+++ b/modules/indexer/issues/db.go
@@ -26,8 +26,8 @@ func (db *DBIndexer) Delete(ids ...int64) error {
}
// Search dummy function
-func (db *DBIndexer) Search(kw string, repoID int64, limit, start int) (*SearchResult, error) {
- total, ids, err := models.SearchIssueIDsByKeyword(kw, repoID, limit, start)
+func (db *DBIndexer) Search(kw string, repoIDs []int64, limit, start int) (*SearchResult, error) {
+ total, ids, err := models.SearchIssueIDsByKeyword(kw, repoIDs, limit, start)
if err != nil {
return nil, err
}
@@ -37,8 +37,7 @@ func (db *DBIndexer) Search(kw string, repoID int64, limit, start int) (*SearchR
}
for _, id := range ids {
result.Hits = append(result.Hits, Match{
- ID: id,
- RepoID: repoID,
+ ID: id,
})
}
return &result, nil
diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go
index 4f410daf4c..76da46d759 100644
--- a/modules/indexer/issues/indexer.go
+++ b/modules/indexer/issues/indexer.go
@@ -28,9 +28,8 @@ type IndexerData struct {
// Match represents on search result
type Match struct {
- ID int64 `json:"id"`
- RepoID int64 `json:"repo_id"`
- Score float64 `json:"score"`
+ ID int64 `json:"id"`
+ Score float64 `json:"score"`
}
// SearchResult represents search results
@@ -44,7 +43,7 @@ type Indexer interface {
Init() (bool, error)
Index(issue []*IndexerData) error
Delete(ids ...int64) error
- Search(kw string, repoID int64, limit, start int) (*SearchResult, error)
+ Search(kw string, repoIDs []int64, limit, start int) (*SearchResult, error)
}
type indexerHolder struct {
@@ -262,9 +261,9 @@ func DeleteRepoIssueIndexer(repo *models.Repository) {
}
// SearchIssuesByKeyword search issue ids by keywords and repo id
-func SearchIssuesByKeyword(repoID int64, keyword string) ([]int64, error) {
+func SearchIssuesByKeyword(repoIDs []int64, keyword string) ([]int64, error) {
var issueIDs []int64
- res, err := holder.get().Search(keyword, repoID, 1000, 0)
+ res, err := holder.get().Search(keyword, repoIDs, 1000, 0)
if err != nil {
return nil, err
}
diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go
index 212c2edfbe..a45fede9ac 100644
--- a/modules/indexer/issues/indexer_test.go
+++ b/modules/indexer/issues/indexer_test.go
@@ -30,19 +30,19 @@ func TestBleveSearchIssues(t *testing.T) {
time.Sleep(5 * time.Second)
- ids, err := SearchIssuesByKeyword(1, "issue2")
+ ids, err := SearchIssuesByKeyword([]int64{1}, "issue2")
assert.NoError(t, err)
assert.EqualValues(t, []int64{2}, ids)
- ids, err = SearchIssuesByKeyword(1, "first")
+ ids, err = SearchIssuesByKeyword([]int64{1}, "first")
assert.NoError(t, err)
assert.EqualValues(t, []int64{1}, ids)
- ids, err = SearchIssuesByKeyword(1, "for")
+ ids, err = SearchIssuesByKeyword([]int64{1}, "for")
assert.NoError(t, err)
assert.EqualValues(t, []int64{1, 2, 3, 5}, ids)
- ids, err = SearchIssuesByKeyword(1, "good")
+ ids, err = SearchIssuesByKeyword([]int64{1}, "good")
assert.NoError(t, err)
assert.EqualValues(t, []int64{1}, ids)
}
@@ -53,19 +53,19 @@ func TestDBSearchIssues(t *testing.T) {
setting.Indexer.IssueType = "db"
InitIssueIndexer(true)
- ids, err := SearchIssuesByKeyword(1, "issue2")
+ ids, err := SearchIssuesByKeyword([]int64{1}, "issue2")
assert.NoError(t, err)
assert.EqualValues(t, []int64{2}, ids)
- ids, err = SearchIssuesByKeyword(1, "first")
+ ids, err = SearchIssuesByKeyword([]int64{1}, "first")
assert.NoError(t, err)
assert.EqualValues(t, []int64{1}, ids)
- ids, err = SearchIssuesByKeyword(1, "for")
+ ids, err = SearchIssuesByKeyword([]int64{1}, "for")
assert.NoError(t, err)
assert.EqualValues(t, []int64{1, 2, 3, 5}, ids)
- ids, err = SearchIssuesByKeyword(1, "good")
+ ids, err = SearchIssuesByKeyword([]int64{1}, "good")
assert.NoError(t, err)
assert.EqualValues(t, []int64{1}, ids)
}
diff --git a/modules/setting/service.go b/modules/setting/service.go
index dea4081ee8..93629100a2 100644
--- a/modules/setting/service.go
+++ b/modules/setting/service.go
@@ -39,6 +39,7 @@ var Service struct {
EnableTimetracking bool
DefaultEnableTimetracking bool
DefaultEnableDependencies bool
+ AllowCrossRepositoryDependencies bool
DefaultAllowOnlyContributorsToTrackTime bool
NoReplyAddress string
EnableUserHeatmap bool
@@ -79,6 +80,7 @@ func newService() {
Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true)
}
Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_DEPENDENCIES").MustBool(true)
+ Service.AllowCrossRepositoryDependencies = sec.Key("ALLOW_CROSS_REPOSITORY_DEPENDENCIES").MustBool(true)
Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true)
Service.NoReplyAddress = sec.Key("NO_REPLY_ADDRESS").MustString("noreply.example.org")
Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true)
diff --git a/modules/structs/issue.go b/modules/structs/issue.go
index 58fd7344b4..bd39f9ea44 100644
--- a/modules/structs/issue.go
+++ b/modules/structs/issue.go
@@ -26,6 +26,13 @@ type PullRequestMeta struct {
Merged *time.Time `json:"merged_at"`
}
+// RepositoryMeta basic repository information
+type RepositoryMeta struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+ FullName string `json:"full_name"`
+}
+
// Issue represents an issue in a repository
// swagger:model
type Issue struct {
@@ -57,6 +64,7 @@ type Issue struct {
Deadline *time.Time `json:"due_date"`
PullRequest *PullRequestMeta `json:"pull_request"`
+ Repo *RepositoryMeta `json:"repository"`
}
// ListIssueOption list issue options
diff --git a/public/css/index.css b/public/css/index.css
index dca2d6f0b6..f7eb02b296 100644
--- a/public/css/index.css
+++ b/public/css/index.css
@@ -78,6 +78,7 @@ a{cursor:pointer}
.ui.form .ui.button{font-weight:400}
.ui.floating.label{z-index:10}
.ui.transparent.label{background-color:transparent}
+.ui.nopadding{padding:0}
.ui.menu,.ui.segment,.ui.vertical.menu{box-shadow:none}
.ui .menu:not(.vertical) .item>.button.compact{padding:.58928571em 1.125em}
.ui .menu:not(.vertical) .item>.button.small{font-size:.92857143rem}
@@ -109,6 +110,8 @@ a{cursor:pointer}
.ui .text.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block}
.ui .text.thin{font-weight:400}
.ui .text.middle{vertical-align:middle}
+.ui .text.nopadding{padding:0}
+.ui .text.nomargin{margin:0}
.ui .message{text-align:center}
.ui.bottom.attached.message{font-weight:700;text-align:left;color:#000}
.ui.bottom.attached.message .pull-right{color:#000}
diff --git a/public/js/index.js b/public/js/index.js
index e76e993a1d..bfcf36f528 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -3254,10 +3254,16 @@ function deleteDependencyModal(id, type) {
function initIssueList() {
const repolink = $('#repolink').val();
+ const repoId = $('#repoId').val();
+ const crossRepoSearch = $('#crossRepoSearch').val();
+ let issueSearchUrl = suburl + '/api/v1/repos/' + repolink + '/issues?q={query}';
+ if (crossRepoSearch === 'true') {
+ issueSearchUrl = suburl + '/api/v1/repos/issues/search?q={query}&priority_repo_id=' + repoId;
+ }
$('#new-dependency-drop-list')
.dropdown({
apiSettings: {
- url: suburl + '/api/v1/repos/' + repolink + '/issues?q={query}',
+ url: issueSearchUrl,
onResponse: function(response) {
const filteredResponse = {'success': true, 'results': []};
const currIssueId = $('#new-dependency-drop-list').data('issue-id');
@@ -3268,7 +3274,8 @@ function initIssueList() {
return;
}
filteredResponse.results.push({
- 'name' : '#' + issue.number + '&nbsp;' + htmlEncode(issue.title),
+ 'name' : '#' + issue.number + ' ' + htmlEncode(issue.title) +
+ '<div class="text small dont-break-out">' + htmlEncode(issue.repository.full_name) + '</div>',
'value' : issue.id
});
});
diff --git a/public/less/_base.less b/public/less/_base.less
index 7fcfaf82ea..8bf49b1ef9 100644
--- a/public/less/_base.less
+++ b/public/less/_base.less
@@ -321,6 +321,10 @@ code,
background-color: transparent;
}
+ &.nopadding {
+ padding: 0;
+ }
+
&.menu,
&.vertical.menu,
&.segment {
@@ -453,6 +457,14 @@ code,
&.middle {
vertical-align: middle;
}
+
+ &.nopadding {
+ padding: 0;
+ }
+
+ &.nomargin {
+ margin: 0;
+ }
}
.message {
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index f8ab9025b7..1acd849b8d 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -596,6 +596,8 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("/search", repo.Search)
})
+ m.Get("/repos/issues/search", repo.SearchIssues)
+
m.Combo("/repositories/:id", reqToken()).Get(repo.GetByID)
m.Group("/repos", func() {
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 426826653c..fe5862ea5e 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
@@ -22,6 +23,137 @@ import (
milestone_service "code.gitea.io/gitea/services/milestone"
)
+// SearchIssues searches for issues across the repositories that the user has access to
+func SearchIssues(ctx *context.APIContext) {
+ // swagger:operation GET /repos/issues/search issue issueSearchIssues
+ // ---
+ // summary: Search for issues across the repositories that the user has access to
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: state
+ // in: query
+ // description: whether issue is open or closed
+ // type: string
+ // - name: labels
+ // in: query
+ // description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded
+ // type: string
+ // - name: page
+ // in: query
+ // description: page number of requested issues
+ // type: integer
+ // - name: q
+ // in: query
+ // description: search string
+ // type: string
+ // - name: priority_repo_id
+ // in: query
+ // description: repository to prioritize in the results
+ // type: integer
+ // format: int64
+ // responses:
+ // "200":
+ // "$ref": "#/responses/IssueList"
+ var isClosed util.OptionalBool
+ switch ctx.Query("state") {
+ case "closed":
+ isClosed = util.OptionalBoolTrue
+ case "all":
+ isClosed = util.OptionalBoolNone
+ default:
+ isClosed = util.OptionalBoolFalse
+ }
+
+ // find repos user can access (for issue search)
+ repoIDs := make([]int64, 0)
+ issueCount := 0
+ for page := 1; ; page++ {
+ repos, count, err := models.SearchRepositoryByName(&models.SearchRepoOptions{
+ Page: page,
+ PageSize: 15,
+ Private: true,
+ Keyword: "",
+ OwnerID: ctx.User.ID,
+ TopicOnly: false,
+ Collaborate: util.OptionalBoolNone,
+ UserIsAdmin: ctx.IsUserSiteAdmin(),
+ UserID: ctx.User.ID,
+ OrderBy: models.SearchOrderByRecentUpdated,
+ })
+ if err != nil {
+ ctx.Error(500, "SearchRepositoryByName", err)
+ return
+ }
+
+ if len(repos) == 0 {
+ break
+ }
+ log.Trace("Processing next %d repos of %d", len(repos), count)
+ for _, repo := range repos {
+ switch isClosed {
+ case util.OptionalBoolTrue:
+ issueCount += repo.NumClosedIssues
+ case util.OptionalBoolFalse:
+ issueCount += repo.NumOpenIssues
+ case util.OptionalBoolNone:
+ issueCount += repo.NumIssues
+ }
+ repoIDs = append(repoIDs, repo.ID)
+ }
+ }
+
+ var issues []*models.Issue
+
+ keyword := strings.Trim(ctx.Query("q"), " ")
+ if strings.IndexByte(keyword, 0) >= 0 {
+ keyword = ""
+ }
+ var issueIDs []int64
+ var labelIDs []int64
+ var err error
+ if len(keyword) > 0 && len(repoIDs) > 0 {
+ issueIDs, err = issue_indexer.SearchIssuesByKeyword(repoIDs, keyword)
+ }
+
+ labels := ctx.Query("labels")
+ if splitted := strings.Split(labels, ","); labels != "" && len(splitted) > 0 {
+ labelIDs, err = models.GetLabelIDsInReposByNames(repoIDs, splitted)
+ if err != nil {
+ ctx.Error(500, "GetLabelIDsInRepoByNames", err)
+ 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 {
+ issues, err = models.Issues(&models.IssuesOptions{
+ RepoIDs: repoIDs,
+ Page: ctx.QueryInt("page"),
+ PageSize: setting.UI.IssuePagingNum,
+ IsClosed: isClosed,
+ IssueIDs: issueIDs,
+ LabelIDs: labelIDs,
+ SortType: "priorityrepo",
+ PriorityRepoID: ctx.QueryInt64("priority_repo_id"),
+ })
+ }
+
+ if err != nil {
+ ctx.Error(500, "Issues", err)
+ return
+ }
+
+ apiIssues := make([]*api.Issue, len(issues))
+ for i := range issues {
+ apiIssues[i] = issues[i].APIFormat()
+ }
+
+ ctx.SetLinkHeader(issueCount, setting.UI.IssuePagingNum)
+ ctx.JSON(200, &apiIssues)
+}
+
// ListIssues list the issues of a repository
func ListIssues(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues issue issueListIssues
@@ -79,7 +211,7 @@ func ListIssues(ctx *context.APIContext) {
var labelIDs []int64
var err error
if len(keyword) > 0 {
- issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx.Repo.Repository.ID, keyword)
+ issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{ctx.Repo.Repository.ID}, keyword)
}
if splitted := strings.Split(ctx.Query("labels"), ","); len(splitted) > 0 {
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index 04c718d5b9..9a691471d5 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -149,7 +149,7 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB
var issueIDs []int64
if len(keyword) > 0 {
- issueIDs, err = issue_indexer.SearchIssuesByKeyword(repo.ID, keyword)
+ issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{repo.ID}, keyword)
if err != nil {
ctx.ServerError("issueIndexer.Search", err)
return
@@ -778,6 +778,9 @@ func ViewIssue(ctx *context.Context) {
// Check if the user can use the dependencies
ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User)
+ // check if dependencies can be created across repositories
+ ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies
+
// Render comments and and fetch participants.
participants[0] = issue.Poster
for _, comment = range issue.Comments {
diff --git a/routers/repo/issue_dependency.go b/routers/repo/issue_dependency.go
index 730271126d..6b11f0cdf1 100644
--- a/routers/repo/issue_dependency.go
+++ b/routers/repo/issue_dependency.go
@@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/setting"
)
// AddDependency adds new dependencies
@@ -39,14 +40,14 @@ func AddDependency(ctx *context.Context) {
return
}
- // Check if both issues are in the same repo
- if issue.RepoID != dep.RepoID {
+ // Check if both issues are in the same repo if cross repository dependencies is not enabled
+ if issue.RepoID != dep.RepoID && !setting.Service.AllowCrossRepositoryDependencies {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_same_repo"))
return
}
// Check if issue and dependency is the same
- if dep.Index == issueIndex {
+ if dep.ID == issue.ID {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_same_issue"))
return
}
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index c42d8aff7f..637d4ad04a 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -274,14 +274,15 @@
</span>
<div class="ui relaxed divided list">
{{range .BlockingDependencies}}
- <div class="item{{if .IsClosed}} is-closed{{end}}">
- <div class="ui black label">#{{.Index}}</div>
- <a class="title has-emoji" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title}}</a>
- <div class="ui transparent label right floated">
+ <div class="item{{if .Issue.IsClosed}} is-closed{{end}}">
+ <span class="text grey right floated">#{{.Issue.Index}}</span>
+ <a class="title has-emoji" href="{{.Repository.Link}}/issues/{{.Issue.Index}}">{{.Issue.Title}}</a>
+ <div class="text small">{{.Repository.OwnerName}}/{{.Repository.Name}}</div>
+ <div class="ui transparent label right floated nopadding">
{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
- <a class="delete-dependency-button" onclick="deleteDependencyModal({{.ID}}, 'blocking');"
- data-tooltip="{{$.i18n.Tr "repo.issues.dependency.remove_info"}}" data-inverted="">
- <i class="delete icon text red"></i>
+ <a class="delete-dependency-button" onclick="deleteDependencyModal({{.Issue.ID}}, 'blocking');"
+ data-tooltip="{{$.i18n.Tr "repo.issues.dependency.remove_info"}}" data-inverted="">
+ <i class="delete icon text red nopadding nomargin"></i>
</a>
{{end}}
</div>
@@ -300,14 +301,15 @@
</span>
<div class="ui relaxed divided list">
{{range .BlockedByDependencies}}
- <div class="item{{if .IsClosed}} is-closed{{end}}">
- <div class="ui black label">#{{.Index}}</div>
- <a class="title has-emoji" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title}}</a>
- <div class="ui transparent label right floated">
- {{if and $.CanCreateIssueDependencies (not $.IsArchived)}}
- <a class="delete-dependency-button" onclick="deleteDependencyModal({{.ID}}, 'blockedBy');"
- data-tooltip="{{$.i18n.Tr "repo.issues.dependency.remove_info"}}" data-inverted="">
- <i class="delete icon text red"></i>
+ <div class="item{{if .Issue.IsClosed}} is-closed{{end}}">
+ <span class="text grey right floated">#{{.Issue.Index}}</span>
+ <a class="title has-emoji" href="{{.Repository.Link}}/issues/{{.Issue.Index}}">{{.Issue.Title}}</a>
+ <div class="text small">{{.Repository.OwnerName}}/{{.Repository.Name}}</div>
+ <div class="ui transparent label right floated nopadding">
+ {{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
+ <a class="delete-dependency-button" onclick="deleteDependencyModal({{.Issue.ID}}, 'blockedBy');"
+ data-tooltip="{{$.i18n.Tr "repo.issues.dependency.remove_info"}}" data-inverted="">
+ <i class="delete icon text red nopadding nomargin"></i>
</a>
{{end}}
</div>
@@ -424,6 +426,8 @@
</div>
{{if and .CanCreateIssueDependencies (not .Repository.IsArchived)}}
<input type="hidden" id="repolink" value="{{$.RepoRelPath}}">
+ <input type="hidden" id="repoId" value="{{.Repository.ID}}">
+ <input type="hidden" id="crossRepoSearch" value="{{.AllowCrossRepositoryDependencies}}">
<!-- I know, there is probably a better way to do this -->
<input type="hidden" id="issueIndex" value="{{.Issue.Index}}"/>
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 5be36d23be..da7ebda852 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -1111,6 +1111,56 @@
}
}
},
+ "/repos/issues/search": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "Search for issues across the repositories that the user has access to",
+ "operationId": "issueSearchIssues",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "whether issue is open or closed",
+ "name": "state",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded",
+ "name": "labels",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "page number of requested issues",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "string",
+ "description": "search string",
+ "name": "q",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "format": "int64",
+ "description": "repository to prioritize in the results",
+ "name": "priority_repo_id",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/IssueList"
+ }
+ }
+ }
+ },
"/repos/migrate": {
"post": {
"consumes": [
@@ -9199,6 +9249,9 @@
"pull_request": {
"$ref": "#/definitions/PullRequestMeta"
},
+ "repository": {
+ "$ref": "#/definitions/RepositoryMeta"
+ },
"state": {
"$ref": "#/definitions/StateType"
},
@@ -10095,6 +10148,26 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "RepositoryMeta": {
+ "description": "RepositoryMeta basic repository information",
+ "type": "object",
+ "properties": {
+ "full_name": {
+ "type": "string",
+ "x-go-name": "FullName"
+ },
+ "id": {
+ "type": "integer",
+ "format": "int64",
+ "x-go-name": "ID"
+ },
+ "name": {
+ "type": "string",
+ "x-go-name": "Name"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"SearchResults": {
"description": "SearchResults results of a successful search",
"type": "object",