Skip to content

Commit 887a8fe

Browse files
bhalbrightlunny
authored andcommitted
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
1 parent 086a469 commit 887a8fe

File tree

22 files changed

+354
-65
lines changed

22 files changed

+354
-65
lines changed

custom/conf/app.ini.sample

+2
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,8 @@ DEFAULT_ORG_MEMBER_VISIBLE = false
479479
; Default value for EnableDependencies
480480
; Repositories will use dependencies by default depending on this setting
481481
DEFAULT_ENABLE_DEPENDENCIES = true
482+
; Dependencies can be added from any repository where the user is granted access or only from the current repository depending on this setting.
483+
ALLOW_CROSS_REPOSITORY_DEPENDENCIES = true
482484
; Enable heatmap on users profiles.
483485
ENABLE_USER_HEATMAP = true
484486
; Enable Timetracking

docs/content/doc/advanced/config-cheat-sheet.en-us.md

+1
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ relation to port exhaustion.
297297
- `RECAPTCHA_SITEKEY`: **""**: Go to https://www.google.com/recaptcha/admin to get a sitekey for recaptcha.
298298
- `RECAPTCHA_URL`: **https://www.google.com/recaptcha/**: Set the recaptcha url - allows the use of recaptcha net.
299299
- `DEFAULT_ENABLE_DEPENDENCIES`: **true**: Enable this to have dependencies enabled by default.
300+
- `ALLOW_CROSS_REPOSITORY_DEPENDENCIES` : **true** Enable this to allow dependencies on issues from any repository where the user is granted access.
300301
- `ENABLE_USER_HEATMAP`: **true**: Enable this to display the heatmap on users profiles.
301302
- `EMAIL_DOMAIN_WHITELIST`: **\<empty\>**: If non-empty, list of domain names that can only be used to register
302303
on this instance.

models/issue.go

+35-14
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"path"
1010
"regexp"
1111
"sort"
12+
"strconv"
1213
"strings"
1314

1415
"code.gitea.io/gitea/modules/base"
@@ -378,6 +379,12 @@ func (issue *Issue) apiFormat(e Engine) *api.Issue {
378379
Updated: issue.UpdatedUnix.AsTime(),
379380
}
380381

382+
apiIssue.Repo = &api.RepositoryMeta{
383+
ID: issue.Repo.ID,
384+
Name: issue.Repo.Name,
385+
FullName: issue.Repo.FullName(),
386+
}
387+
381388
if issue.ClosedUnix != 0 {
382389
apiIssue.Closed = issue.ClosedUnix.AsTimePtr()
383390
}
@@ -1047,11 +1054,13 @@ type IssuesOptions struct {
10471054
LabelIDs []int64
10481055
SortType string
10491056
IssueIDs []int64
1057+
// prioritize issues from this repo
1058+
PriorityRepoID int64
10501059
}
10511060

10521061
// sortIssuesSession sort an issues-related session based on the provided
10531062
// sortType string
1054-
func sortIssuesSession(sess *xorm.Session, sortType string) {
1063+
func sortIssuesSession(sess *xorm.Session, sortType string, priorityRepoID int64) {
10551064
switch sortType {
10561065
case "oldest":
10571066
sess.Asc("issue.created_unix")
@@ -1069,6 +1078,8 @@ func sortIssuesSession(sess *xorm.Session, sortType string) {
10691078
sess.Asc("issue.deadline_unix")
10701079
case "farduedate":
10711080
sess.Desc("issue.deadline_unix")
1081+
case "priorityrepo":
1082+
sess.OrderBy("CASE WHEN issue.repo_id = " + strconv.FormatInt(priorityRepoID, 10) + " THEN 1 ELSE 2 END, issue.created_unix DESC")
10721083
default:
10731084
sess.Desc("issue.created_unix")
10741085
}
@@ -1170,7 +1181,7 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) {
11701181
defer sess.Close()
11711182

11721183
opts.setupSession(sess)
1173-
sortIssuesSession(sess, opts.SortType)
1184+
sortIssuesSession(sess, opts.SortType, opts.PriorityRepoID)
11741185

11751186
issues := make([]*Issue, 0, setting.UI.IssuePagingNum)
11761187
if err := sess.Find(&issues); err != nil {
@@ -1476,8 +1487,8 @@ func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen
14761487
}
14771488

14781489
// SearchIssueIDsByKeyword search issues on database
1479-
func SearchIssueIDsByKeyword(kw string, repoID int64, limit, start int) (int64, []int64, error) {
1480-
var repoCond = builder.Eq{"repo_id": repoID}
1490+
func SearchIssueIDsByKeyword(kw string, repoIDs []int64, limit, start int) (int64, []int64, error) {
1491+
var repoCond = builder.In("repo_id", repoIDs)
14811492
var subQuery = builder.Select("id").From("issue").Where(repoCond)
14821493
var cond = builder.And(
14831494
repoCond,
@@ -1566,33 +1577,43 @@ func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *Us
15661577
return sess.Commit()
15671578
}
15681579

1580+
// DependencyInfo represents high level information about an issue which is a dependency of another issue.
1581+
type DependencyInfo struct {
1582+
Issue `xorm:"extends"`
1583+
Repository `xorm:"extends"`
1584+
}
1585+
15691586
// Get Blocked By Dependencies, aka all issues this issue is blocked by.
1570-
func (issue *Issue) getBlockedByDependencies(e Engine) (issueDeps []*Issue, err error) {
1587+
func (issue *Issue) getBlockedByDependencies(e Engine) (issueDeps []*DependencyInfo, err error) {
15711588
return issueDeps, e.
1572-
Table("issue_dependency").
1573-
Select("issue.*").
1574-
Join("INNER", "issue", "issue.id = issue_dependency.dependency_id").
1589+
Table("issue").
1590+
Join("INNER", "repository", "repository.id = issue.repo_id").
1591+
Join("INNER", "issue_dependency", "issue_dependency.dependency_id = issue.id").
15751592
Where("issue_id = ?", issue.ID).
1593+
//sort by repo id then created date, with the issues of the same repo at the beginning of the list
1594+
OrderBy("CASE WHEN issue.repo_id = " + strconv.FormatInt(issue.RepoID, 10) + " THEN 0 ELSE issue.repo_id END, issue.created_unix DESC").
15761595
Find(&issueDeps)
15771596
}
15781597

15791598
// Get Blocking Dependencies, aka all issues this issue blocks.
1580-
func (issue *Issue) getBlockingDependencies(e Engine) (issueDeps []*Issue, err error) {
1599+
func (issue *Issue) getBlockingDependencies(e Engine) (issueDeps []*DependencyInfo, err error) {
15811600
return issueDeps, e.
1582-
Table("issue_dependency").
1583-
Select("issue.*").
1584-
Join("INNER", "issue", "issue.id = issue_dependency.issue_id").
1601+
Table("issue").
1602+
Join("INNER", "repository", "repository.id = issue.repo_id").
1603+
Join("INNER", "issue_dependency", "issue_dependency.issue_id = issue.id").
15851604
Where("dependency_id = ?", issue.ID).
1605+
//sort by repo id then created date, with the issues of the same repo at the beginning of the list
1606+
OrderBy("CASE WHEN issue.repo_id = " + strconv.FormatInt(issue.RepoID, 10) + " THEN 0 ELSE issue.repo_id END, issue.created_unix DESC").
15861607
Find(&issueDeps)
15871608
}
15881609

15891610
// BlockedByDependencies finds all Dependencies an issue is blocked by
1590-
func (issue *Issue) BlockedByDependencies() ([]*Issue, error) {
1611+
func (issue *Issue) BlockedByDependencies() ([]*DependencyInfo, error) {
15911612
return issue.getBlockedByDependencies(x)
15921613
}
15931614

15941615
// BlockingDependencies returns all blocking dependencies, aka all other issues a given issue blocks
1595-
func (issue *Issue) BlockingDependencies() ([]*Issue, error) {
1616+
func (issue *Issue) BlockingDependencies() ([]*DependencyInfo, error) {
15961617
return issue.getBlockingDependencies(x)
15971618
}
15981619

models/issue_label.go

+13
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,19 @@ func GetLabelIDsInRepoByNames(repoID int64, labelNames []string) ([]int64, error
250250
Find(&labelIDs)
251251
}
252252

253+
// GetLabelIDsInReposByNames returns a list of labelIDs by names in one of the given
254+
// repositories.
255+
// it silently ignores label names that do not belong to the repository.
256+
func GetLabelIDsInReposByNames(repoIDs []int64, labelNames []string) ([]int64, error) {
257+
labelIDs := make([]int64, 0, len(labelNames))
258+
return labelIDs, x.Table("label").
259+
In("repo_id", repoIDs).
260+
In("name", labelNames).
261+
Asc("name").
262+
Cols("id").
263+
Find(&labelIDs)
264+
}
265+
253266
// GetLabelInRepoByID returns a label by ID in given repository.
254267
func GetLabelInRepoByID(repoID, labelID int64) (*Label, error) {
255268
return getLabelInRepoByID(x, repoID, labelID)

models/issue_test.go

+4-5
Original file line numberDiff line numberDiff line change
@@ -264,24 +264,23 @@ func TestIssue_loadTotalTimes(t *testing.T) {
264264

265265
func TestIssue_SearchIssueIDsByKeyword(t *testing.T) {
266266
assert.NoError(t, PrepareTestDatabase())
267-
268-
total, ids, err := SearchIssueIDsByKeyword("issue2", 1, 10, 0)
267+
total, ids, err := SearchIssueIDsByKeyword("issue2", []int64{1}, 10, 0)
269268
assert.NoError(t, err)
270269
assert.EqualValues(t, 1, total)
271270
assert.EqualValues(t, []int64{2}, ids)
272271

273-
total, ids, err = SearchIssueIDsByKeyword("first", 1, 10, 0)
272+
total, ids, err = SearchIssueIDsByKeyword("first", []int64{1}, 10, 0)
274273
assert.NoError(t, err)
275274
assert.EqualValues(t, 1, total)
276275
assert.EqualValues(t, []int64{1}, ids)
277276

278-
total, ids, err = SearchIssueIDsByKeyword("for", 1, 10, 0)
277+
total, ids, err = SearchIssueIDsByKeyword("for", []int64{1}, 10, 0)
279278
assert.NoError(t, err)
280279
assert.EqualValues(t, 4, total)
281280
assert.EqualValues(t, []int64{1, 2, 3, 5}, ids)
282281

283282
// issue1's comment id 2
284-
total, ids, err = SearchIssueIDsByKeyword("good", 1, 10, 0)
283+
total, ids, err = SearchIssueIDsByKeyword("good", []int64{1}, 10, 0)
285284
assert.NoError(t, err)
286285
assert.EqualValues(t, 1, total)
287286
assert.EqualValues(t, []int64{1}, ids)

models/pull_list.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func PullRequests(baseRepoID int64, opts *PullRequestsOptions) ([]*PullRequest,
8787

8888
prs := make([]*PullRequest, 0, ItemsPerPage)
8989
findSession, err := listPullRequestStatement(baseRepoID, opts)
90-
sortIssuesSession(findSession, opts.SortType)
90+
sortIssuesSession(findSession, opts.SortType, 0)
9191
if err != nil {
9292
log.Error("listPullRequestStatement: %v", err)
9393
return nil, maxResults, err

modules/indexer/issues/bleve.go

+12-4
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,18 @@ func (b *BleveIndexer) Delete(ids ...int64) error {
218218

219219
// Search searches for issues by given conditions.
220220
// Returns the matching issue IDs
221-
func (b *BleveIndexer) Search(keyword string, repoID int64, limit, start int) (*SearchResult, error) {
221+
func (b *BleveIndexer) Search(keyword string, repoIDs []int64, limit, start int) (*SearchResult, error) {
222+
var repoQueriesP []*query.NumericRangeQuery
223+
for _, repoID := range repoIDs {
224+
repoQueriesP = append(repoQueriesP, numericEqualityQuery(repoID, "RepoID"))
225+
}
226+
repoQueries := make([]query.Query, len(repoQueriesP))
227+
for i, v := range repoQueriesP {
228+
repoQueries[i] = query.Query(v)
229+
}
230+
222231
indexerQuery := bleve.NewConjunctionQuery(
223-
numericEqualityQuery(repoID, "RepoID"),
232+
bleve.NewDisjunctionQuery(repoQueries...),
224233
bleve.NewDisjunctionQuery(
225234
newMatchPhraseQuery(keyword, "Title", issueIndexerAnalyzer),
226235
newMatchPhraseQuery(keyword, "Content", issueIndexerAnalyzer),
@@ -242,8 +251,7 @@ func (b *BleveIndexer) Search(keyword string, repoID int64, limit, start int) (*
242251
return nil, err
243252
}
244253
ret.Hits = append(ret.Hits, Match{
245-
ID: id,
246-
RepoID: repoID,
254+
ID: id,
247255
})
248256
}
249257
return &ret, nil

modules/indexer/issues/bleve_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func TestBleveIndexAndSearch(t *testing.T) {
7676
)
7777

7878
for _, kw := range keywords {
79-
res, err := indexer.Search(kw.Keyword, 2, 10, 0)
79+
res, err := indexer.Search(kw.Keyword, []int64{2}, 10, 0)
8080
assert.NoError(t, err)
8181

8282
var ids = make([]int64, 0, len(res.Hits))

modules/indexer/issues/db.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ func (db *DBIndexer) Delete(ids ...int64) error {
2626
}
2727

2828
// Search dummy function
29-
func (db *DBIndexer) Search(kw string, repoID int64, limit, start int) (*SearchResult, error) {
30-
total, ids, err := models.SearchIssueIDsByKeyword(kw, repoID, limit, start)
29+
func (db *DBIndexer) Search(kw string, repoIDs []int64, limit, start int) (*SearchResult, error) {
30+
total, ids, err := models.SearchIssueIDsByKeyword(kw, repoIDs, limit, start)
3131
if err != nil {
3232
return nil, err
3333
}
@@ -37,8 +37,7 @@ func (db *DBIndexer) Search(kw string, repoID int64, limit, start int) (*SearchR
3737
}
3838
for _, id := range ids {
3939
result.Hits = append(result.Hits, Match{
40-
ID: id,
41-
RepoID: repoID,
40+
ID: id,
4241
})
4342
}
4443
return &result, nil

modules/indexer/issues/indexer.go

+5-6
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ type IndexerData struct {
2828

2929
// Match represents on search result
3030
type Match struct {
31-
ID int64 `json:"id"`
32-
RepoID int64 `json:"repo_id"`
33-
Score float64 `json:"score"`
31+
ID int64 `json:"id"`
32+
Score float64 `json:"score"`
3433
}
3534

3635
// SearchResult represents search results
@@ -44,7 +43,7 @@ type Indexer interface {
4443
Init() (bool, error)
4544
Index(issue []*IndexerData) error
4645
Delete(ids ...int64) error
47-
Search(kw string, repoID int64, limit, start int) (*SearchResult, error)
46+
Search(kw string, repoIDs []int64, limit, start int) (*SearchResult, error)
4847
}
4948

5049
type indexerHolder struct {
@@ -262,9 +261,9 @@ func DeleteRepoIssueIndexer(repo *models.Repository) {
262261
}
263262

264263
// SearchIssuesByKeyword search issue ids by keywords and repo id
265-
func SearchIssuesByKeyword(repoID int64, keyword string) ([]int64, error) {
264+
func SearchIssuesByKeyword(repoIDs []int64, keyword string) ([]int64, error) {
266265
var issueIDs []int64
267-
res, err := holder.get().Search(keyword, repoID, 1000, 0)
266+
res, err := holder.get().Search(keyword, repoIDs, 1000, 0)
268267
if err != nil {
269268
return nil, err
270269
}

modules/indexer/issues/indexer_test.go

+8-8
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,19 @@ func TestBleveSearchIssues(t *testing.T) {
3030

3131
time.Sleep(5 * time.Second)
3232

33-
ids, err := SearchIssuesByKeyword(1, "issue2")
33+
ids, err := SearchIssuesByKeyword([]int64{1}, "issue2")
3434
assert.NoError(t, err)
3535
assert.EqualValues(t, []int64{2}, ids)
3636

37-
ids, err = SearchIssuesByKeyword(1, "first")
37+
ids, err = SearchIssuesByKeyword([]int64{1}, "first")
3838
assert.NoError(t, err)
3939
assert.EqualValues(t, []int64{1}, ids)
4040

41-
ids, err = SearchIssuesByKeyword(1, "for")
41+
ids, err = SearchIssuesByKeyword([]int64{1}, "for")
4242
assert.NoError(t, err)
4343
assert.EqualValues(t, []int64{1, 2, 3, 5}, ids)
4444

45-
ids, err = SearchIssuesByKeyword(1, "good")
45+
ids, err = SearchIssuesByKeyword([]int64{1}, "good")
4646
assert.NoError(t, err)
4747
assert.EqualValues(t, []int64{1}, ids)
4848
}
@@ -53,19 +53,19 @@ func TestDBSearchIssues(t *testing.T) {
5353
setting.Indexer.IssueType = "db"
5454
InitIssueIndexer(true)
5555

56-
ids, err := SearchIssuesByKeyword(1, "issue2")
56+
ids, err := SearchIssuesByKeyword([]int64{1}, "issue2")
5757
assert.NoError(t, err)
5858
assert.EqualValues(t, []int64{2}, ids)
5959

60-
ids, err = SearchIssuesByKeyword(1, "first")
60+
ids, err = SearchIssuesByKeyword([]int64{1}, "first")
6161
assert.NoError(t, err)
6262
assert.EqualValues(t, []int64{1}, ids)
6363

64-
ids, err = SearchIssuesByKeyword(1, "for")
64+
ids, err = SearchIssuesByKeyword([]int64{1}, "for")
6565
assert.NoError(t, err)
6666
assert.EqualValues(t, []int64{1, 2, 3, 5}, ids)
6767

68-
ids, err = SearchIssuesByKeyword(1, "good")
68+
ids, err = SearchIssuesByKeyword([]int64{1}, "good")
6969
assert.NoError(t, err)
7070
assert.EqualValues(t, []int64{1}, ids)
7171
}

modules/setting/service.go

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ var Service struct {
3939
EnableTimetracking bool
4040
DefaultEnableTimetracking bool
4141
DefaultEnableDependencies bool
42+
AllowCrossRepositoryDependencies bool
4243
DefaultAllowOnlyContributorsToTrackTime bool
4344
NoReplyAddress string
4445
EnableUserHeatmap bool
@@ -79,6 +80,7 @@ func newService() {
7980
Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true)
8081
}
8182
Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_DEPENDENCIES").MustBool(true)
83+
Service.AllowCrossRepositoryDependencies = sec.Key("ALLOW_CROSS_REPOSITORY_DEPENDENCIES").MustBool(true)
8284
Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true)
8385
Service.NoReplyAddress = sec.Key("NO_REPLY_ADDRESS").MustString("noreply.example.org")
8486
Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true)

modules/structs/issue.go

+8
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ type PullRequestMeta struct {
2626
Merged *time.Time `json:"merged_at"`
2727
}
2828

29+
// RepositoryMeta basic repository information
30+
type RepositoryMeta struct {
31+
ID int64 `json:"id"`
32+
Name string `json:"name"`
33+
FullName string `json:"full_name"`
34+
}
35+
2936
// Issue represents an issue in a repository
3037
// swagger:model
3138
type Issue struct {
@@ -57,6 +64,7 @@ type Issue struct {
5764
Deadline *time.Time `json:"due_date"`
5865

5966
PullRequest *PullRequestMeta `json:"pull_request"`
67+
Repo *RepositoryMeta `json:"repository"`
6068
}
6169

6270
// ListIssueOption list issue options

public/css/index.css

+3
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ a{cursor:pointer}
7878
.ui.form .ui.button{font-weight:400}
7979
.ui.floating.label{z-index:10}
8080
.ui.transparent.label{background-color:transparent}
81+
.ui.nopadding{padding:0}
8182
.ui.menu,.ui.segment,.ui.vertical.menu{box-shadow:none}
8283
.ui .menu:not(.vertical) .item>.button.compact{padding:.58928571em 1.125em}
8384
.ui .menu:not(.vertical) .item>.button.small{font-size:.92857143rem}
@@ -109,6 +110,8 @@ a{cursor:pointer}
109110
.ui .text.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block}
110111
.ui .text.thin{font-weight:400}
111112
.ui .text.middle{vertical-align:middle}
113+
.ui .text.nopadding{padding:0}
114+
.ui .text.nomargin{margin:0}
112115
.ui .message{text-align:center}
113116
.ui.bottom.attached.message{font-weight:700;text-align:left;color:#000}
114117
.ui.bottom.attached.message .pull-right{color:#000}

0 commit comments

Comments
 (0)