Browse Source

Merge f40747596a into 993736d838

pull/29117/merge
Tim-Niclas Oelschläger 1 week ago
parent
commit
731d422f36
No account linked to committer's email address

+ 17
- 0
models/fixtures/issue_dependency.yml View File

@@ -0,0 +1,17 @@
-
id: 1
user_id: 40
issue_id: 21
dependency_id: 20

-
id: 2
user_id: 40
issue_id: 21
dependency_id: 22

-
id: 3
user_id: 40
issue_id: 20
dependency_id: 22

+ 2
- 2
models/issues/dependency.go View File

@@ -107,8 +107,8 @@ func (err ErrUnknownDependencyType) Unwrap() error {
type IssueDependency struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"NOT NULL"`
IssueID int64 `xorm:"UNIQUE(issue_dependency) NOT NULL"`
DependencyID int64 `xorm:"UNIQUE(issue_dependency) NOT NULL"`
IssueID int64 `xorm:"UNIQUE(issue_dependency) NOT NULL index"`
DependencyID int64 `xorm:"UNIQUE(issue_dependency) NOT NULL index"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}

+ 7
- 2
models/issues/issue.go View File

@@ -580,9 +580,14 @@ func IsUserParticipantsOfIssue(ctx context.Context, user *user_model.User, issue
}

// DependencyInfo represents high level information about an issue which is a dependency of another issue.
// this type is used in func `BlockingDependenciesMap` and `BlockedByDependenciesMap` as xorm intermediate type to retrieve info from joined tables
type DependencyInfo struct {
Issue `xorm:"extends"`
repo_model.Repository `xorm:"extends"`
Issue `xorm:"extends"` // an issue/pull that depend on issue_id or is blocked by issue_id. the exact usage is determined by the function using this type
repo_model.Repository `xorm:"extends"` // the repo, that owns Issue

// fields from `IssueDependency`
IssueID int64 `xorm:"NOT NULL"` // id of the issue/pull the that is used for the selection of dependent issues
DependencyID int64 `xorm:"NOT NULL"` // id of the issue/pull the that is used for the selection of blocked issues
}

// GetParticipantIDsByIssue returns all userIDs who are participated in comments of an issue and issue author

+ 52
- 0
models/issues/issue_list.go View File

@@ -626,3 +626,55 @@ func (issues IssueList) LoadIsRead(ctx context.Context, userID int64) error {

return nil
}

func (issues IssueList) BlockingDependenciesMap(ctx context.Context) (issueDepsMap map[int64][]*DependencyInfo, err error) {
var issueDeps []*DependencyInfo

err = db.GetEngine(ctx).
Table("issue").
Join("INNER", "repository", "repository.id = issue.repo_id").
Join("INNER", "issue_dependency", "issue_dependency.issue_id = issue.id").
Where(builder.In("issue_dependency.dependency_id", issues.getIssueIDs())).
// sort by repo id then index
Asc("`issue`.`repo_id`").
Asc("`issue`.`index`").
Find(&issueDeps)
if err != nil {
return nil, err
}

issueDepsMap = make(map[int64][]*DependencyInfo, len(issues))
for _, depInfo := range issueDeps {
depInfo.Issue.Repo = &depInfo.Repository

issueDepsMap[depInfo.DependencyID] = append(issueDepsMap[depInfo.DependencyID], depInfo)
}

return issueDepsMap, nil
}

func (issues IssueList) BlockedByDependenciesMap(ctx context.Context) (issueDepsMap map[int64][]*DependencyInfo, err error) {
var issueDeps []*DependencyInfo

err = db.GetEngine(ctx).
Table("issue").
Join("INNER", "repository", "repository.id = issue.repo_id").
Join("INNER", "issue_dependency", "issue_dependency.dependency_id = issue.id").
Where(builder.In("issue_dependency.issue_id", issues.getIssueIDs())).
// sort by repo id then index
Asc("`issue`.`repo_id`").
Asc("`issue`.`index`").
Find(&issueDeps)
if err != nil {
return nil, err
}

issueDepsMap = make(map[int64][]*DependencyInfo, len(issues))
for _, depInfo := range issueDeps {
depInfo.Issue.Repo = &depInfo.Repository

issueDepsMap[depInfo.IssueID] = append(issueDepsMap[depInfo.IssueID], depInfo)
}

return issueDepsMap, nil
}

+ 136
- 0
models/issues/issue_list_test.go View File

@@ -4,10 +4,13 @@
package issues_test

import (
"cmp"
"slices"
"testing"

"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"

@@ -73,3 +76,136 @@ func TestIssueList_LoadAttributes(t *testing.T) {
}
}
}

func TestIssueList_BlockingDependenciesMap(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
issueList := issues_model.IssueList{
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 20}),
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 21}),
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 22}),
}

blockingDependenciesMap, err := issueList.BlockingDependenciesMap(db.DefaultContext)
assert.NoError(t, err)
if assert.Len(t, blockingDependenciesMap, 2) {
var keys []int64
for k := range blockingDependenciesMap {
keys = append(keys, k)
}
slices.Sort(keys)
assert.EqualValues(t, []int64{20, 22}, keys)

if assert.Len(t, blockingDependenciesMap[20], 1) {
expectIssuesDependencyInfo(t,
&issues_model.DependencyInfo{
IssueID: 21,
DependencyID: 20,
Issue: issues_model.Issue{ID: 21},
Repository: repo_model.Repository{ID: 60},
},
blockingDependenciesMap[20][0])
}
if assert.Len(t, blockingDependenciesMap[22], 2) {
list := sortIssuesDependencyInfos(blockingDependenciesMap[22])
expectIssuesDependencyInfo(t, &issues_model.DependencyInfo{
IssueID: 20,
DependencyID: 22,
Issue: issues_model.Issue{ID: 20},
Repository: repo_model.Repository{ID: 23},
}, list[0])
expectIssuesDependencyInfo(t, &issues_model.DependencyInfo{
IssueID: 21,
DependencyID: 22,
Issue: issues_model.Issue{ID: 21},
Repository: repo_model.Repository{ID: 60},
}, list[1])
}
}

issueList = issues_model.IssueList{
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 21}),
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 22}),
}

blockingDependenciesMap, err = issueList.BlockingDependenciesMap(db.DefaultContext)
assert.NoError(t, err)
assert.Len(t, blockingDependenciesMap, 1)
assert.Len(t, blockingDependenciesMap[22], 2)
}

func TestIssueList_BlockedByDependenciesMap(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
issueList := issues_model.IssueList{
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 20}),
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 21}),
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 22}),
}

blockedByDependenciesMap, err := issueList.BlockedByDependenciesMap(db.DefaultContext)
assert.NoError(t, err)
if assert.Len(t, blockedByDependenciesMap, 2) {
var keys []int64
for k := range blockedByDependenciesMap {
keys = append(keys, k)
}
slices.Sort(keys)
assert.EqualValues(t, []int64{20, 21}, keys)

if assert.Len(t, blockedByDependenciesMap[20], 1) {
expectIssuesDependencyInfo(t,
&issues_model.DependencyInfo{
IssueID: 20,
DependencyID: 22,
Issue: issues_model.Issue{ID: 22},
Repository: repo_model.Repository{ID: 61},
},
blockedByDependenciesMap[20][0])
}
if assert.Len(t, blockedByDependenciesMap[21], 2) {
list := sortIssuesDependencyInfos(blockedByDependenciesMap[21])
expectIssuesDependencyInfo(t, &issues_model.DependencyInfo{
IssueID: 21,
DependencyID: 20,
Issue: issues_model.Issue{ID: 20},
Repository: repo_model.Repository{ID: 23},
}, list[0])
expectIssuesDependencyInfo(t, &issues_model.DependencyInfo{
IssueID: 21,
DependencyID: 22,
Issue: issues_model.Issue{ID: 22},
Repository: repo_model.Repository{ID: 61},
}, list[1])
}
}

issueList = issues_model.IssueList{
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 21}),
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 22}),
}

blockedByDependenciesMap, err = issueList.BlockedByDependenciesMap(db.DefaultContext)
assert.NoError(t, err)
assert.Len(t, blockedByDependenciesMap, 1)
assert.Len(t, blockedByDependenciesMap[21], 2)
}

func expectIssuesDependencyInfo(t *testing.T, expect, got *issues_model.DependencyInfo) {
if expect == nil {
assert.Nil(t, got)
return
}
if !assert.NotNil(t, got) {
return
}
assert.EqualValues(t, expect.DependencyID, got.DependencyID, "DependencyID")
assert.EqualValues(t, expect.IssueID, got.IssueID, "IssueID")
assert.EqualValues(t, expect.Issue.ID, got.Issue.ID, "RelatedIssueID")
assert.EqualValues(t, expect.Repository.ID, got.Repository.ID, "RelatedIssueRepoID")
}

func sortIssuesDependencyInfos(in []*issues_model.DependencyInfo) []*issues_model.DependencyInfo {
slices.SortFunc(in, func(a, b *issues_model.DependencyInfo) int {
return cmp.Compare(a.DependencyID, b.DependencyID)
})
return in
}

+ 1
- 0
models/repo/repo_unit.go View File

@@ -120,6 +120,7 @@ func (cfg *IssuesConfig) ToDB() ([]byte, error) {
// PullRequestsConfig describes pull requests config
type PullRequestsConfig struct {
IgnoreWhitespaceConflicts bool
ShowDependencies bool
AllowMerge bool
AllowRebase bool
AllowRebaseMerge bool

+ 3
- 0
options/locale/locale_en-US.ini View File

@@ -1703,7 +1703,9 @@ issues.dependency.issue_close_blocked = You need to close all issues blocking th
issues.dependency.issue_batch_close_blocked = "Cannot batch close issues that you choose, because issue #%d still has open dependencies"
issues.dependency.pr_close_blocked = You need to close all issues blocking this pull request before you can merge it.
issues.dependency.blocks_short = Blocks
issues.dependency.blocks_following = blocks:
issues.dependency.blocked_by_short = Depends on
issues.dependency.blocked_by_following = depends on:
issues.dependency.remove_header = Remove Dependency
issues.dependency.issue_remove_text = This will remove the dependency from this issue. Continue?
issues.dependency.pr_remove_text = This will remove the dependency from this pull request. Continue?
@@ -2126,6 +2128,7 @@ settings.enable_timetracker = Enable Time Tracking
settings.allow_only_contributors_to_track_time = Let Only Contributors Track Time
settings.pulls_desc = Enable Repository Pull Requests
settings.pulls.ignore_whitespace = Ignore Whitespace for Conflicts
settings.pulls.show_dependencies = Show <b>depends on</b> and <b>blocks</b> in Pull Requests list
settings.pulls.enable_autodetect_manual_merge = Enable autodetect manual merge (Note: In some special cases, misjudgments can occur)
settings.pulls.allow_rebase_update = Enable updating pull request branch by rebase
settings.pulls.default_delete_branch_after_merge = Delete pull request branch after merge by default

+ 1
- 0
public/assets/img/svg/gitea-issue-dependency.svg View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg gitea-issue-dependency" width="16" height="16" aria-hidden="true"><path d="m11.804 14.134-1.727-1.816h4.787a1.105 1.105 0 0 0 0-2.209h-4.787l1.727-1.815a1.105 1.105 0 1 0-1.6-1.523L6.7 10.453a1.105 1.105 0 0 0 0 1.522l3.502 3.682a1.105 1.105 0 0 0 1.601-1.523z"/><path d="M15.964 2.535A2.535 2.535 0 0 0 13.43 0H2.567A2.536 2.536 0 0 0 .032 2.535v10.862a2.535 2.535 0 0 0 2.535 2.535H4.74a1.086 1.086 0 0 0 0-2.173H2.567a.36.36 0 0 1-.362-.362V2.535a.36.36 0 0 1 .362-.362H13.43a.36.36 0 0 1 .362.362v2.172a1.086 1.086 0 0 0 2.172 0z"/></svg>

+ 1
- 0
public/assets/img/svg/gitea-issue-dependent.svg View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg gitea-issue-dependent" width="16" height="16" aria-hidden="true"><path d="m10.56 14.134 1.728-1.816H7.5a1.105 1.105 0 0 1 0-2.209h4.788L10.56 8.294a1.105 1.105 0 1 1 1.601-1.523l3.502 3.682a1.105 1.105 0 0 1 0 1.522l-3.502 3.682a1.105 1.105 0 0 1-1.6-1.523Z"/><path d="M15.964 2.535A2.535 2.535 0 0 0 13.43 0H2.567A2.536 2.536 0 0 0 .032 2.535v10.862a2.535 2.535 0 0 0 2.535 2.535H4.74a1.086 1.086 0 0 0 0-2.173H2.567a.36.36 0 0 1-.362-.362V2.535a.36.36 0 0 1 .362-.362H13.43a.36.36 0 0 1 .362.362v2.172a1.086 1.086 0 0 0 2.172 0z"/></svg>

+ 19
- 0
routers/web/repo/issue.go View File

@@ -323,6 +323,25 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
return
}

if unit, err := repo.GetUnit(ctx, unit.TypePullRequests); err == nil {
if config := unit.PullRequestsConfig(); config.ShowDependencies {
blockingDependenciesMap, err := issues.BlockingDependenciesMap(ctx)
if err != nil {
ctx.ServerError("BlockingDependenciesMap", err)
return
}

blockedByDependenciesMap, err := issues.BlockedByDependenciesMap(ctx)
if err != nil {
ctx.ServerError("BlockedByDependenciesMap", err)
return
}

ctx.Data["BlockingDependenciesMap"] = blockingDependenciesMap
ctx.Data["BlockedByDependenciesMap"] = blockedByDependenciesMap
}
}

if ctx.IsSigned {
if err := issues.LoadIsRead(ctx, ctx.Doer.ID); err != nil {
ctx.ServerError("LoadIsRead", err)

+ 1
- 0
routers/web/repo/setting/setting.go View File

@@ -586,6 +586,7 @@ func SettingsPost(ctx *context.Context) {
Type: unit_model.TypePullRequests,
Config: &repo_model.PullRequestsConfig{
IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace,
ShowDependencies: form.PullsShowDependencies,
AllowMerge: form.PullsAllowMerge,
AllowRebase: form.PullsAllowRebase,
AllowRebaseMerge: form.PullsAllowRebaseMerge,

+ 1
- 0
services/forms/repo_form.go View File

@@ -150,6 +150,7 @@ type RepoSettingForm struct {
EnablePulls bool
EnableActions bool
PullsIgnoreWhitespace bool
PullsShowDependencies bool
PullsAllowMerge bool
PullsAllowRebase bool
PullsAllowRebaseMerge bool

+ 6
- 0
templates/repo/settings/options.tmpl View File

@@ -659,6 +659,12 @@
<label>{{ctx.Locale.Tr "repo.settings.pulls.ignore_whitespace"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="pulls_show_dependencies" type="checkbox" {{if and $pullRequestEnabled ($prUnit.PullRequestsConfig.ShowDependencies)}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.pulls.show_dependencies"}}</label>
</div>
</div>
</div>
{{end}}


+ 9
- 0
templates/shared/issue_dependency.tmpl View File

@@ -0,0 +1,9 @@
{{if .Dependencies}}
<div class="flex-text-inline">
{{ctx.Locale.Tr .TitleKey}}
{{range $i, $dependency := .Dependencies}}
{{if gt $i 0}}<span class="-tw-ml-1">, </span>{{end}}
{{template "shared/issue_link" $dependency.Issue}}
{{end}}
</div>
{{end}}

+ 1
- 0
templates/shared/issue_link.tmpl View File

@@ -0,0 +1 @@
<a href="{{.Link}}" class="{{if .IsClosed}}tw-line-through {{end}}tw-ml-1 ref-issue">#{{.Index}}</a>

+ 10
- 0
templates/shared/issuelist.tmpl View File

@@ -121,6 +121,16 @@
</span>
</span>
{{end}}
{{if $.BlockedByDependenciesMap}}
{{template "shared/issue_dependency" (dict
"Dependencies" (index $.BlockedByDependenciesMap .ID)
"TitleKey" "repo.issues.dependency.blocked_by_following")}}
{{end}}
{{if $.BlockingDependenciesMap}}
{{template "shared/issue_dependency" (dict
"Dependencies" (index $.BlockingDependenciesMap .ID)
"TitleKey" "repo.issues.dependency.blocks_following")}}
{{end}}
{{if .IsPull}}
{{$approveOfficial := call $approvalCounts .ID "approve"}}
{{$rejectOfficial := call $approvalCounts .ID "reject"}}

+ 1
- 0
web_src/svg/gitea-issue-dependency.svg View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m11.804 14.134-1.727-1.816h4.787a1.105 1.105 0 0 0 0-2.209h-4.787l1.727-1.815a1.105 1.105 0 1 0-1.6-1.523L6.7 10.453a1.105 1.105 0 0 0 0 1.522l3.502 3.682a1.105 1.105 0 0 0 1.601-1.523z"/><path d="M15.964 2.535A2.535 2.535 0 0 0 13.43 0H2.567A2.536 2.536 0 0 0 .032 2.535v10.862a2.535 2.535 0 0 0 2.535 2.535H4.74a1.086 1.086 0 0 0 0-2.173H2.567a.362.362 0 0 1-.362-.362V2.535a.362.362 0 0 1 .362-.362H13.43a.362.362 0 0 1 .362.362v2.172a1.086 1.086 0 0 0 2.172 0z"/></svg>

+ 1
- 0
web_src/svg/gitea-issue-dependent.svg View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m10.56 14.134 1.728-1.816H7.5a1.105 1.105 0 0 1 0-2.209h4.788L10.56 8.294a1.105 1.105 0 1 1 1.601-1.523l3.502 3.682a1.105 1.105 0 0 1 0 1.522l-3.502 3.682a1.105 1.105 0 0 1-1.6-1.523Z"/><path d="M15.964 2.535A2.535 2.535 0 0 0 13.43 0H2.567A2.536 2.536 0 0 0 .032 2.535v10.862a2.535 2.535 0 0 0 2.535 2.535H4.74a1.086 1.086 0 0 0 0-2.173H2.567a.362.362 0 0 1-.362-.362V2.535a.362.362 0 0 1 .362-.362H13.43a.362.362 0 0 1 .362.362v2.172a1.086 1.086 0 0 0 2.172 0z"/></svg>

Loading…
Cancel
Save