mirror of https://github.com/go-gitea/gitea
Merge f40747596a
into eb24d973b0
This commit is contained in:
commit
fc9b04cf84
|
@ -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
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1701,7 +1701,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?
|
||||
|
@ -2124,6 +2126,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
|
||||
|
|
|
@ -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>
|
After Width: | Height: | Size: 613 B |
|
@ -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>
|
After Width: | Height: | Size: 610 B |
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -150,6 +150,7 @@ type RepoSettingForm struct {
|
|||
EnablePulls bool
|
||||
EnableActions bool
|
||||
PullsIgnoreWhitespace bool
|
||||
PullsShowDependencies bool
|
||||
PullsAllowMerge bool
|
||||
PullsAllowRebase bool
|
||||
PullsAllowRebaseMerge bool
|
||||
|
|
|
@ -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}}
|
||||
|
||||
|
|
|
@ -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}}
|
|
@ -0,0 +1 @@
|
|||
<a href="{{.Link}}" class="{{if .IsClosed}}tw-line-through {{end}}tw-ml-1 ref-issue">#{{.Index}}</a>
|
|
@ -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"}}
|
||||
|
|
|
@ -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>
|
After Width: | Height: | Size: 542 B |
|
@ -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>
|
After Width: | Height: | Size: 540 B |
Loading…
Reference in New Issue