Browse Source

Начальный функционал для подзадач #6

pull/28/head
parent
commit
e5039e9956
  1. 7
      custom/conf/app.example.ini
  2. 2
      integrations/api_repo_edit_test.go
  3. 85
      models/error.go
  4. 52
      models/issue.go
  5. 38
      models/issue_comment.go
  6. 134
      models/issue_parent.go
  7. 61
      models/issue_parent_test.go
  8. 3
      models/migrations/v70.go
  9. 1
      models/repo.go
  10. 16
      models/repo/issue.go
  11. 1
      models/repo/repo_unit.go
  12. 5
      modules/context/repo.go
  13. 1
      modules/convert/repository.go
  14. 4
      modules/doctor/fix16961.go
  15. 4
      modules/doctor/fix16961_test.go
  16. 4
      modules/setting/service.go
  17. 2
      modules/structs/repo.go
  18. 28
      options/locale/locale_ru-RU.ini
  19. 2
      routers/api/v1/repo/repo.go
  20. 21
      routers/web/repo/issue.go
  21. 129
      routers/web/repo/issue_parent.go
  22. 1
      routers/web/repo/setting.go
  23. 4
      routers/web/web.go
  24. 1
      services/forms/repo_form.go
  25. 2
      templates/admin/config.tmpl
  26. 125
      templates/repo/issue/view_content/sidebar.tmpl
  27. 6
      templates/repo/settings/options.tmpl
  28. 5
      templates/swagger/v1_json.tmpl
  29. 46
      web_src/js/features/repo-issue.js
  30. 3
      web_src/js/features/repo-legacy.js
  31. 15
      web_src/less/_repository.less

7
custom/conf/app.example.ini

@ -701,6 +701,13 @@ PATH =
;; 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
;;
;; Default value for EnableParents
;; Repositories will use parents by default depending on this setting
;DEFAULT_ENABLE_PARENTS = true
;;
;; Parents 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_PARENTS = true
;;
;; Enable heatmap on users profiles.
;ENABLE_USER_HEATMAP = true
;;

2
integrations/api_repo_edit_test.go

@ -35,6 +35,7 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption
EnableTimeTracker: config.EnableTimetracker,
AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime,
EnableIssueDependencies: config.EnableDependencies,
EnableIssueParents: config.EnableParents,
}
} else if unit, err := repo.GetUnit(unit_model.TypeExternalTracker); err == nil {
config := unit.ExternalTrackerConfig()
@ -182,6 +183,7 @@ func TestAPIRepoEdit(t *testing.T) {
EnableTimeTracker: false,
AllowOnlyContributorsToTrackTime: false,
EnableIssueDependencies: false,
EnableIssueParents: false,
}
*repoEditOption.HasWiki = true
repoEditOption.ExternalWiki = nil

85
models/error.go

@ -1340,6 +1340,91 @@ func (err ErrUnknownDependencyType) Error() string {
return fmt.Sprintf("unknown dependency type [type: %d]", err.Type)
}
// .___ ________ .___ .__
// | | ______ ________ __ ____ \______ \ ____ ______ ____ ____ __| _/____ ____ ____ |__| ____ ______
// | |/ ___// ___/ | \_/ __ \ | | \_/ __ \\____ \_/ __ \ / \ / __ |/ __ \ / \_/ ___\| |/ __ \ / ___/
// | |\___ \ \___ \| | /\ ___/ | ` \ ___/| |_> > ___/| | \/ /_/ \ ___/| | \ \___| \ ___/ \___ \
// |___/____ >____ >____/ \___ >_______ /\___ > __/ \___ >___| /\____ |\___ >___| /\___ >__|\___ >____ >
// \/ \/ \/ \/ \/|__| \/ \/ \/ \/ \/ \/ \/ \/
// ErrParentExists represents a "ParentAlreadyExists" kind of error.
type ErrParentExists struct {
IssueID int64
ParentID int64
}
// IsErrParentExists checks if an error is a ErrParentExists.
func IsErrParentExists(err error) bool {
_, ok := err.(ErrParentExists)
return ok
}
func (err ErrParentExists) Error() string {
return fmt.Sprintf("issue parent does already exist [issue id: %d, parent id: %d]", err.IssueID, err.ParentID)
}
// ErrParentNotExists represents a "ParentAlreadyExists" kind of error.
type ErrParentNotExists struct {
IssueID int64
ParentID int64
}
// IsErrParentNotExists checks if an error is a ErrParentExists.
func IsErrParentNotExists(err error) bool {
_, ok := err.(ErrParentNotExists)
return ok
}
func (err ErrParentNotExists) Error() string {
return fmt.Sprintf("issue parent does not exist [issue id: %d, parent id: %d]", err.IssueID, err.ParentID)
}
// ErrCircularParent represents a "ParentCircular" kind of error.
type ErrCircularParent struct {
IssueID int64
ParentID int64
}
// IsErrCircularParent checks if an error is a ErrCircularParent.
func IsErrCircularParent(err error) bool {
_, ok := err.(ErrCircularParent)
return ok
}
func (err ErrCircularParent) Error() string {
return fmt.Sprintf("circular parents exists (two issues blocking each other) [issue id: %d, parent id: %d]", err.IssueID, err.ParentID)
}
// ErrParentsLeft represents an error where the issue you're trying to close still has parents left.
type ErrParentsLeft struct {
IssueID int64
}
// IsErrParentsLeft checks if an error is a ErrParentsLeft.
func IsErrParentsLeft(err error) bool {
_, ok := err.(ErrParentsLeft)
return ok
}
func (err ErrParentsLeft) Error() string {
return fmt.Sprintf("issue has open parents [issue id: %d]", err.IssueID)
}
// ErrUnknownParentType represents an error where an unknown parent type was passed
type ErrUnknownParentType struct {
Type ParentType
}
// IsErrUnknownParentType checks if an error is ErrUnknownParentType
func IsErrUnknownParentType(err error) bool {
_, ok := err.(ErrUnknownParentType)
return ok
}
func (err ErrUnknownParentType) Error() string {
return fmt.Sprintf("unknown parent type [type: %d]", err.Type)
}
// __________ .__
// \______ \ _______ _|__| ______ _ __
// | _// __ \ \/ / |/ __ \ \/ \/ /

52
models/issue.go

@ -1990,6 +1990,12 @@ type DependencyInfo struct {
repo_model.Repository `xorm:"extends"`
}
// ParentInfo represents high level information about an issue which is a parent of another issue.
type ParentInfo struct {
Issue `xorm:"extends"`
repo_model.Repository `xorm:"extends"`
}
// getParticipantIDsByIssue returns all userIDs who are participated in comments of an issue and issue author
func (issue *Issue) getParticipantIDsByIssue(e db.Engine) ([]int64, error) {
if issue == nil {
@ -2048,6 +2054,42 @@ func (issue *Issue) getBlockingDependencies(e db.Engine) (issueDeps []*Dependenc
return issueDeps, err
}
// Get Blocked By Parents, aka all issues this issue is blocked by.
func (issue *Issue) getBlockedByParents(e db.Engine) (issueParents []*ParentInfo, err error) {
err = e.
Table("issue").
Join("INNER", "repository", "repository.id = issue.repo_id").
Join("INNER", "issue_parent", "issue_parent.parent_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(&issueParents)
for _, parentInfo := range issueParents {
parentInfo.Issue.Repo = &parentInfo.Repository
}
return issueParents, err
}
// Get Blocking Parents, aka all issues this issue blocks.
func (issue *Issue) getBlockingParents(e db.Engine) (issueParents []*ParentInfo, err error) {
err = e.
Table("issue").
Join("INNER", "repository", "repository.id = issue.repo_id").
Join("INNER", "issue_parent", "issue_parent.issue_id = issue.id").
Where("parent_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(&issueParents)
for _, parentInfo := range issueParents {
parentInfo.Issue.Repo = &parentInfo.Repository
}
return issueParents, err
}
// BlockedByDependencies finds all Dependencies an issue is blocked by
func (issue *Issue) BlockedByDependencies() ([]*DependencyInfo, error) {
return issue.getBlockedByDependencies(db.GetEngine(db.DefaultContext))
@ -2058,6 +2100,16 @@ func (issue *Issue) BlockingDependencies() ([]*DependencyInfo, error) {
return issue.getBlockingDependencies(db.GetEngine(db.DefaultContext))
}
// BlockedByParents finds all Parents an issue is blocked by
func (issue *Issue) BlockedByParents() ([]*ParentInfo, error) {
return issue.getBlockedByParents(db.GetEngine(db.DefaultContext))
}
// BlockingParents returns all blocking dependencies, aka all other issues a given issue blocks
func (issue *Issue) BlockingParents() ([]*ParentInfo, error) {
return issue.getBlockingParents(db.GetEngine(db.DefaultContext))
}
func (issue *Issue) updateClosedNum(ctx context.Context) (err error) {
if issue.IsPull {
err = repoStatsCorrectNumClosed(ctx, issue.RepoID, true, "num_closed_pulls")

38
models/issue_comment.go

@ -82,6 +82,10 @@ const (
CommentTypeAddDependency
// 20 Dependency removed
CommentTypeRemoveDependency
// 19 Parent added
CommentTypeAddParent
// 20 Parent removed
CommentTypeRemoveParent
// 21 Comment a line of code
CommentTypeCode
// 22 Reviews a pull request by giving general feedback
@ -933,6 +937,39 @@ func createIssueDependencyComment(ctx context.Context, doer *user_model.User, is
return
}
// Creates issue parent comment
func createIssueParentComment(ctx context.Context, doer *user_model.User, issue, parentIssue *Issue, add bool) (err error) {
cType := CommentTypeAddParent
if !add {
cType = CommentTypeRemoveParent
}
if err = issue.loadRepo(ctx); err != nil {
return
}
// Make two comments, one in each issue
opts := &CreateCommentOptions{
Type: cType,
Doer: doer,
Repo: issue.Repo,
Issue: issue,
ParentIssueID: parentIssue.ID,
}
if _, err = createComment(ctx, opts); err != nil {
return
}
opts = &CreateCommentOptions{
Type: cType,
Doer: doer,
Repo: issue.Repo,
Issue: parentIssue,
ParentIssueID: issue.ID,
}
_, err = createComment(ctx, opts)
return
}
// CreateCommentOptions defines options for creating comment
type CreateCommentOptions struct {
Type CommentType
@ -942,6 +979,7 @@ type CreateCommentOptions struct {
Label *Label
DependentIssueID int64
ParentIssueID int64
OldMilestoneID int64
MilestoneID int64
OldProjectID int64

134
models/issue_parent.go

@ -0,0 +1,134 @@
// Copyright 2018-2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil"
)
// IssueParent represents an issue parent
type IssueParent struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"NOT NULL"`
IssueID int64 `xorm:"UNIQUE(issue_parent) NOT NULL"`
ParentID int64 `xorm:"UNIQUE(issue_parent) NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(IssueParent))
}
// ParentType Defines Parent Type Constants
type ParentType int
// Define Parent Types
const (
ParentTypeFather ParentType = iota
ParentTypeChild
)
// CreateIssueParent creates a new parent for an issue
func CreateIssueParent(user *user_model.User, issue, parent *Issue) error {
ctx, committer, err := db.TxContext()
if err != nil {
return err
}
defer committer.Close()
sess := db.GetEngine(ctx)
// Check if it aleready exists
exists, err := issueParentExists(sess, issue.ID, parent.ID)
if err != nil {
return err
}
if exists {
return ErrParentExists{issue.ID, parent.ID}
}
// And if it would be circular
circular, err := issueParentExists(sess, parent.ID, issue.ID)
if err != nil {
return err
}
if circular {
return ErrCircularParent{issue.ID, parent.ID}
}
if err := db.Insert(ctx, &IssueParent{
UserID: user.ID,
IssueID: issue.ID,
ParentID: parent.ID,
}); err != nil {
return err
}
// Add comment referencing the new parent
if err = createIssueParentComment(ctx, user, issue, parent, true); err != nil {
return err
}
return committer.Commit()
}
// RemoveIssueParent removes a parent from an issue
func RemoveIssueParent(user *user_model.User, issue, parent *Issue, parentType ParentType) (err error) {
ctx, committer, err := db.TxContext()
if err != nil {
return err
}
defer committer.Close()
var issueParentToDelete IssueParent
switch parentType {
case ParentTypeFather:
issueParentToDelete = IssueParent{IssueID: issue.ID, ParentID: parent.ID}
case ParentTypeChild:
issueParentToDelete = IssueParent{IssueID: parent.ID, ParentID: issue.ID}
default:
return ErrUnknownParentType{parentType}
}
affected, err := db.GetEngine(ctx).Delete(&issueParentToDelete)
if err != nil {
return err
}
// If we deleted nothing, the parent did not exist
if affected <= 0 {
return ErrParentNotExists{issue.ID, parent.ID}
}
// Add comment referencing the removed parent
if err = createIssueParentComment(ctx, user, issue, parent, false); err != nil {
return err
}
return committer.Commit()
}
// Check if the parent already exists
func issueParentExists(e db.Engine, issueID, depID int64) (bool, error) {
return e.Where("(issue_id = ? AND parent_id = ?)", issueID, depID).Exist(&IssueParent{})
}
// IssueNoParentsLeft checks if issue can be closed
func IssueNoParentsLeft(issue *Issue) (bool, error) {
return issueNoParentsLeft(db.GetEngine(db.DefaultContext), issue)
}
func issueNoParentsLeft(e db.Engine, issue *Issue) (bool, error) {
exists, err := e.
Table("issue_parent").
Select("issue.*").
Join("INNER", "issue", "issue.id = issue_parent.parent_id").
Where("issue_parent.issue_id = ?", issue.ID).
And("issue.is_closed = ?", "0").
Exist(&Issue{})
return !exists, err
}

61
models/issue_parent_test.go

@ -0,0 +1,61 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"testing"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestCreateIssueParent(t *testing.T) {
// Prepare
assert.NoError(t, unittest.PrepareTestDatabase())
user1, err := user_model.GetUserByID(1)
assert.NoError(t, err)
issue1, err := GetIssueByID(1)
assert.NoError(t, err)
issue2, err := GetIssueByID(2)
assert.NoError(t, err)
// Create a dependency and check if it was successful
err = CreateIssueParent(user1, issue1, issue2)
assert.NoError(t, err)
// Do it again to see if it will check if the dependency already exists
err = CreateIssueParent(user1, issue1, issue2)
assert.Error(t, err)
assert.True(t, IsErrParentExists(err))
// Check for circular dependencies
err = CreateIssueParent(user1, issue2, issue1)
assert.Error(t, err)
assert.True(t, IsErrCircularParent(err))
_ = unittest.AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeAddParent, PosterID: user1.ID, IssueID: issue1.ID})
// Check if dependencies left is correct
left, err := IssueNoParentsLeft(issue1)
assert.NoError(t, err)
assert.False(t, left)
// Close #2 and check again
_, err = issue2.ChangeStatus(user1, true)
assert.NoError(t, err)
left, err = IssueNoParentsLeft(issue1)
assert.NoError(t, err)
assert.True(t, left)
// Test removing the dependency
err = RemoveIssueParent(user1, issue1, issue2, ParentTypeFather)
assert.NoError(t, err)
}

3
models/migrations/v70.go

@ -102,6 +102,9 @@ func addIssueDependencies(x *xorm.Engine) (err error) {
if _, ok := unit.Config["EnableDependencies"]; !ok {
unit.Config["EnableDependencies"] = setting.Service.DefaultEnableDependencies
}
if _, ok := unit.Config["EnableParents"]; !ok {
unit.Config["EnableParents"] = setting.Service.DefaultEnableParents
}
if _, err := x.ID(unit.ID).Cols("config").Update(unit); err != nil {
return err
}

1
models/repo.go

@ -515,6 +515,7 @@ func CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_
EnableTimetracker: setting.Service.DefaultEnableTimetracking,
AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime,
EnableDependencies: setting.Service.DefaultEnableDependencies,
EnableParents: setting.Service.DefaultEnableParents,
},
})
} else if tp == unit.TypePullRequests {

16
models/repo/issue.go

@ -70,3 +70,19 @@ func (repo *Repository) IsDependenciesEnabledCtx(ctx context.Context) bool {
}
return u.IssuesConfig().EnableDependencies
}
// IsParentsEnabled returns if parents are enabled and returns the default setting if not set.
func (repo *Repository) IsParentsEnabled() bool {
return repo.IsParentsEnabledCtx(db.DefaultContext)
}
// IsParentsEnabledCtx returns if parents are enabled and returns the default setting if not set.
func (repo *Repository) IsParentsEnabledCtx(ctx context.Context) bool {
var u *RepoUnit
var err error
if u, err = repo.GetUnitCtx(ctx, unit.TypeIssues); err != nil {
log.Trace("%s", err)
return setting.Service.DefaultEnableParents
}
return u.IssuesConfig().EnableParents
}

1
models/repo/repo_unit.go

@ -94,6 +94,7 @@ type IssuesConfig struct {
EnableTimetracker bool
AllowOnlyContributorsToTrackTime bool
EnableDependencies bool
EnableParents bool
}
// FromDB fills up a IssuesConfig from serialized format.

5
modules/context/repo.go

@ -164,6 +164,11 @@ func (r *Repository) CanCreateIssueDependencies(user *user_model.User, isPull bo
return r.Repository.IsDependenciesEnabled() && r.Permission.CanWriteIssuesOrPulls(isPull)
}
// CanCreateIssueParents returns whether or not a user can create parents.
func (r *Repository) CanCreateIssueParents(user *user_model.User, isPull bool) bool {
return r.Repository.IsParentsEnabled() && r.Permission.CanWriteIssuesOrPulls(isPull)
}
// GetCommitsCount returns cached commit count for current view
func (r *Repository) GetCommitsCount() (int64, error) {
var contextName string

1
modules/convert/repository.go

@ -51,6 +51,7 @@ func innerToRepo(repo *repo_model.Repository, mode perm.AccessMode, isParent boo
EnableTimeTracker: config.EnableTimetracker,
AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime,
EnableIssueDependencies: config.EnableDependencies,
EnableIssueParents: config.EnableParents,
}
} else if unit, err := repo.GetUnit(unit_model.TypeExternalTracker); err == nil {
config := unit.ExternalTrackerConfig()

4
modules/doctor/fix16961.go

@ -206,6 +206,10 @@ func fixIssuesConfig16961(bs []byte, cfg *repo_model.IssuesConfig) (fixed bool,
if parseErr != nil {
return
}
cfg.EnableParents, parseErr = parseBool16961(parts[3])
if parseErr != nil {
return
}
return true, nil
}

4
modules/doctor/fix16961_test.go

@ -237,11 +237,12 @@ func Test_fixIssuesConfig_16961(t *testing.T) {
}{
{
name: "normal",
bs: `{"EnableTimetracker":true,"AllowOnlyContributorsToTrackTime":true,"EnableDependencies":true}`,
bs: `{"EnableTimetracker":true,"AllowOnlyContributorsToTrackTime":true,"EnableDependencies":true,"EnableParents":true}`,
expected: repo_model.IssuesConfig{
EnableTimetracker: true,
AllowOnlyContributorsToTrackTime: true,
EnableDependencies: true,
EnableParents: true,
},
},
{
@ -251,6 +252,7 @@ func Test_fixIssuesConfig_16961(t *testing.T) {
EnableTimetracker: true,
AllowOnlyContributorsToTrackTime: true,
EnableDependencies: true,
EnableParents: true,
},
wantFixed: true,
},

4
modules/setting/service.go

@ -53,7 +53,9 @@ var Service = struct {
EnableTimetracking bool
DefaultEnableTimetracking bool
DefaultEnableDependencies bool
DefaultEnableParents bool
AllowCrossRepositoryDependencies bool
AllowCrossRepositoryParents bool
DefaultAllowOnlyContributorsToTrackTime bool
NoReplyAddress string
EnableUserHeatmap bool
@ -141,7 +143,9 @@ func newService() {
Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true)
}
Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_DEPENDENCIES").MustBool(true)
Service.DefaultEnableParents = sec.Key("DEFAULT_ENABLE_PARENTS").MustBool(true)
Service.AllowCrossRepositoryDependencies = sec.Key("ALLOW_CROSS_REPOSITORY_DEPENDENCIES").MustBool(true)
Service.AllowCrossRepositoryParents = sec.Key("ALLOW_CROSS_REPOSITORY_PARENTS").MustBool(true)
Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true)
Service.NoReplyAddress = sec.Key("NO_REPLY_ADDRESS").MustString("noreply." + Domain)
Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true)

2
modules/structs/repo.go

@ -25,6 +25,8 @@ type InternalTracker struct {
AllowOnlyContributorsToTrackTime bool `json:"allow_only_contributors_to_track_time"`
// Enable dependencies for issues and pull requests (Built-in issue tracker)
EnableIssueDependencies bool `json:"enable_issue_dependencies"`
// Enable parents for issues and pull requests (Built-in issue tracker)
EnableIssueParents bool `json:"enable_issue_parents"`
}
// ExternalTracker represents settings for external tracker

28
options/locale/locale_ru-RU.ini

@ -1372,6 +1372,33 @@ issues.dependency.add_error_dep_not_exist=Зависимости не сущес
issues.dependency.add_error_dep_exists=Зависимость уже существует.
issues.dependency.add_error_cannot_create_circular=Вы не можете создать зависимость с двумя задачами, блокирующими друг друга.
issues.dependency.add_error_dep_not_same_repo=Обе задачи должны находиться в одном репозитории.
issues.parent.title=Родительские задачи
issues.parent.issue_no_parents=В настоящее время эта задача не имеет родителей.
issues.parent.pr_no_parents=Этот запрос на слияние в настоящее время не имеет никаких родителей.
issues.parent.add=Добавить родителя…
issues.parent.cancel=Отменить
issues.parent.remove=Удалить
issues.parent.remove_info=Удалить этого родителя
issues.parent.added_parent=`добавить нового родителя %s`
issues.parent.removed_parent=`убрал родителя %s`
issues.parent.pr_closing_blockedby=Этот запрос на слияние имеет родителей
issues.parent.issue_closing_blockedby=Эта задача имеет родителей
issues.parent.issue_close_blocks=Эта задача имеет следующих детей
issues.parent.pr_close_blocks=Этот запрос на слияние имеет следующих детей
issues.parent.blocks_short=Дети
issues.parent.blocked_by_short=Родители
issues.parent.remove_header=Удалить родителя
issues.parent.issue_remove_text=Это приведет к удалению родителя от этой задачи. Продолжить?
issues.parent.pr_remove_text=Это приведёт к удалению родителя от этого запроса на слияние. Продолжить?
issues.parent.setting=Включение родителя для задач и запросов на слияние
issues.parent.add_error_same_issue=Вы не можете указать родителя задачи на саму себя.
issues.parent.add_error_dep_issue_not_exist=Родительсткая задача не существует.
issues.parent.add_error_dep_not_exist=Родительсткой задачи не существует.
issues.parent.add_error_dep_exists=Привязка к родителю уже существует.
issues.parent.add_error_cannot_create_circular=Вы не можете создать родителей с двумя задачами, блокирующими друг друга.
issues.parent.add_error_dep_not_same_repo=Обе задачи должны находиться в одном репозитории.
issues.review.self.approval=Вы не можете одобрить собственный запрос на слияние.
issues.review.self.rejection=Невозможно запрашивать изменения своего запроса на слияние.
issues.review.approve=одобрил(а) эти изменения %s
@ -2623,6 +2650,7 @@ config.default_allow_only_contributors_to_track_time=Учитывать толь
config.no_reply_address=No-reply адрес
config.default_visibility_organization=Видимость по умолчанию для новых организаций
config.default_enable_dependencies=Включение зависимостей для задач по умолчанию
config.default_enable_parents=Включение родителей для задач по умолчанию
config.webhook_config=Конфигурация вебхуков
config.queue_length=Длина очереди

2
routers/api/v1/repo/repo.go

@ -776,6 +776,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
EnableTimetracker: opts.InternalTracker.EnableTimeTracker,
AllowOnlyContributorsToTrackTime: opts.InternalTracker.AllowOnlyContributorsToTrackTime,
EnableDependencies: opts.InternalTracker.EnableIssueDependencies,
EnableParents: opts.InternalTracker.EnableIssueParents,
}
} else if unit, err := repo.GetUnit(unit_model.TypeIssues); err != nil {
// Unit type doesn't exist so we make a new config file with default values
@ -783,6 +784,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
EnableTimetracker: true,
AllowOnlyContributorsToTrackTime: true,
EnableDependencies: true,
EnableParents: true,
}
} else {
config = unit.IssuesConfig()

21
routers/web/repo/issue.go

@ -701,6 +701,9 @@ func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull
// Contains true if the user can create issue dependencies
ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User, isPull)
// Contains true if the user can create issue parents
ctx.Data["CanCreateIssueParents"] = ctx.Repo.CanCreateIssueParents(ctx.User, isPull)
return labels
}
@ -1324,6 +1327,12 @@ func ViewIssue(ctx *context.Context) {
// check if dependencies can be created across repositories
ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies
// Check if the user can use the parents
ctx.Data["CanCreateIssueParents"] = ctx.Repo.CanCreateIssueParents(ctx.User, issue.IsPull)
// check if parents can be created across repositories
ctx.Data["AllowCrossRepositoryParents"] = setting.Service.AllowCrossRepositoryParents
if issue.ShowRole, err = roleDescriptor(repo, issue.Poster, issue); err != nil {
ctx.ServerError("roleDescriptor", err)
return
@ -1650,6 +1659,18 @@ func ViewIssue(ctx *context.Context) {
return
}
// Get Parents
ctx.Data["BlockedByParents"], err = issue.BlockedByParents()
if err != nil {
ctx.ServerError("BlockedByParents", err)
return
}
ctx.Data["BlockingParents"], err = issue.BlockingParents()
if err != nil {
ctx.ServerError("BlockingParents", err)
return
}
ctx.Data["Participants"] = participants
ctx.Data["NumParticipants"] = len(participants)
ctx.Data["Issue"] = issue

129
routers/web/repo/issue_parent.go

@ -0,0 +1,129 @@
// Copyright 2018-2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package repo
import (
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
)
// AddParent adds new parents
func AddParent(ctx *context.Context) {
issueIndex := ctx.ParamsInt64("index")
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex)
if err != nil {
ctx.ServerError("GetIssueByIndex", err)
return
}
// Check if the Repo is allowed to have parents
if !ctx.Repo.CanCreateIssueParents(ctx.User, issue.IsPull) {
ctx.Error(http.StatusForbidden, "CanCreateIssueParents")
return
}
parentID := ctx.FormInt64("newParent")
if err = issue.LoadRepo(); err != nil {
ctx.ServerError("LoadRepo", err)
return
}
// Redirect
defer ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
// Parent
parent, err := models.GetIssueByID(parentID)
if err != nil {
ctx.Flash.Error(ctx.Tr("repo.issues.parent.add_error_dep_issue_not_exist"))
return
}
// Check if both issues are in the same repo if cross repository parents is not enabled
if issue.RepoID != parent.RepoID && !setting.Service.AllowCrossRepositoryParents {
ctx.Flash.Error(ctx.Tr("repo.issues.parent.add_error_dep_not_same_repo"))
return
}
// Check if issue and parent is the same
if parent.ID == issue.ID {
ctx.Flash.Error(ctx.Tr("repo.issues.parent.add_error_same_issue"))
return
}
err = models.CreateIssueParent(ctx.User, issue, parent)
if err != nil {
if models.IsErrParentExists(err) {
ctx.Flash.Error(ctx.Tr("repo.issues.parent.add_error_dep_exists"))
return
} else if models.IsErrCircularParent(err) {
ctx.Flash.Error(ctx.Tr("repo.issues.parent.add_error_cannot_create_circular"))
return
} else {
ctx.ServerError("CreateOrUpdateIssueParent", err)
return
}
}
}
// RemoveParent removes the parent
func RemoveParent(ctx *context.Context) {
issueIndex := ctx.ParamsInt64("index")
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex)
if err != nil {
ctx.ServerError("GetIssueByIndex", err)
return
}
// Check if the Repo is allowed to have dependencies
if !ctx.Repo.CanCreateIssueParents(ctx.User, issue.IsPull) {
ctx.Error(http.StatusForbidden, "CanCreateIssueParents")
return
}
parentID := ctx.FormInt64("removeParentID")
if err = issue.LoadRepo(); err != nil {
ctx.ServerError("LoadRepo", err)
return
}
// Parent Type
parentTypeStr := ctx.Req.PostForm.Get("parentType")
var parentType models.ParentType
switch parentTypeStr {
case "father":
parentType = models.ParentTypeFather
case "child":
parentType = models.ParentTypeChild
default:
ctx.Error(http.StatusBadRequest, "GetDependecyType")
return
}
// Parent
parent, err := models.GetIssueByID(parentID)
if err != nil {
ctx.ServerError("GetIssueByID", err)
return
}
if err = models.RemoveIssueParent(ctx.User, issue, parent, parentType); err != nil {
if models.IsErrParentNotExists(err) {
ctx.Flash.Error(ctx.Tr("repo.issues.parent.add_error_dep_not_exist"))
return
}
ctx.ServerError("RemoveIssueParent", err)
return
}
// Redirect
ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
}

1
routers/web/repo/setting.go

@ -434,6 +434,7 @@ func SettingsPost(ctx *context.Context) {
EnableTimetracker: form.EnableTimetracker,
AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
EnableDependencies: form.EnableIssueDependencies,
EnableParents: form.EnableIssueParents,
},
})
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)

4
routers/web/web.go

@ -741,6 +741,10 @@ func RegisterRoutes(m *web.Route) {
m.Post("/add", repo.AddDependency)
m.Post("/delete", repo.RemoveDependency)
})
m.Group("/parent", func() {
m.Post("/add", repo.AddParent)
m.Post("/delete", repo.RemoveParent)
})
m.Combo("/comments").Post(repo.MustAllowUserComment, bindIgnErr(forms.CreateCommentForm{}), repo.NewComment)
m.Group("/times", func() {
m.Post("/add", bindIgnErr(forms.AddTimeManuallyForm{}), repo.AddTimeManually)

1
services/forms/repo_form.go

@ -155,6 +155,7 @@ type RepoSettingForm struct {
EnableTimetracker bool
AllowOnlyContributorsToTrackTime bool
EnableIssueDependencies bool
EnableIssueParents bool
IsArchived bool
// Signing Settings

2
templates/admin/config.tmpl

@ -186,6 +186,8 @@
<dd>{{if .Service.NoReplyAddress}}{{.Service.NoReplyAddress}}{{else}}-{{end}}</dd>
<dt>{{.i18n.Tr "admin.config.default_enable_dependencies"}}</dt>
<dd>{{if .Service.DefaultEnableDependencies}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
<dt>{{.i18n.Tr "admin.config.default_enable_parents"}}</dt>
<dd>{{if .Service.DefaultEnableParents}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</dd>
<div class="ui divider"></div>
<dt>{{.i18n.Tr "admin.config.active_code_lives"}}</dt>
<dd>{{.Service.ActiveCodeLives}} {{.i18n.Tr "tool.raw_minutes"}}</dd>

125
templates/repo/issue/view_content/sidebar.tmpl

@ -443,6 +443,131 @@
</div>
{{end}}
</div>
{{if .Repository.IsParentsEnabled}}
<div class="ui divider"></div>
<div class="ui depending">
{{if (and (not .BlockedByParents) (not .BlockingParents))}}
<span class="text"><strong>{{.i18n.Tr "repo.issues.parent.title"}}</strong></span>
<br>
<p>
{{if .Issue.IsPull}}
{{.i18n.Tr "repo.issues.parent.pr_no_parents"}}
{{else}}
{{.i18n.Tr "repo.issues.parent.issue_no_parents"}}
{{end}}
</p>
{{end}}
{{if .BlockingParents}}
<span class="text tooltip" data-content="{{if .Issue.IsPull}}{{.i18n.Tr "repo.issues.parent.pr_close_blocks"}}{{else}}{{.i18n.Tr "repo.issues.parent.issue_close_blocks"}}{{end}}">
<strong>{{.i18n.Tr "repo.issues.parent.blocks_short"}}</strong>
</span>
<div class="ui relaxed divided list">
{{range .BlockingParents}}
<div class="item parent{{if .Issue.IsClosed}} is-closed{{end}} df ac sb">
<div class="item-left df jc fc f1">
<a class="title" href="{{.Issue.Link}}">
#{{.Issue.Index}} {{.Issue.Title | RenderEmoji}}
</a>
<div class="text small">
{{.Repository.OwnerName}}/{{.Repository.Name}}
</div>
</div>
<div class="item-right df ac">
{{if and $.CanCreateIssueParents (not $.Repository.IsArchived)}}
<a class="delete-parent-button tooltip ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-content="{{$.i18n.Tr "repo.issues.parent.remove_info"}}" data-inverted="">
{{svg "octicon-trash" 16}}
</a>
{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
{{if .BlockedByParents}}
<span class="text tooltip" data-content="{{if .Issue.IsPull}}{{.i18n.Tr "repo.issues.parent.pr_closing_blockedby"}}{{else}}{{.i18n.Tr "repo.issues.parent.issue_closing_blockedby"}}{{end}}">
<strong>{{.i18n.Tr "repo.issues.parent.blocked_by_short"}}</strong>
</span>
<div class="ui relaxed divided list">
{{range .BlockedByParents}}
<div class="item parent{{if .Issue.IsClosed}} is-closed{{end}} df ac sb">
<div class="item-left df jc fc f1">
<a class="title" href="{{.Issue.Link}}">
#{{.Issue.Index}} {{.Issue.Title | RenderEmoji}}
</a>
<div class="text small">
{{.Repository.OwnerName}}/{{.Repository.Name}}
</div>
</div>
<div class="item-right df ac">
{{if and $.CanCreateIssueParents (not $.Repository.IsArchived)}}
<a class="delete-parent-button tooltip ci muted" data-id="{{.Issue.ID}}" data-type="blockedBy" data-content="{{$.i18n.Tr "repo.issues.parent.remove_info"}}" data-inverted="">
{{svg "octicon-trash" 16}}
</a>
{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
{{if and .CanCreateIssueParents (not .Repository.IsArchived)}}
<div>
<form method="POST" action="{{.Issue.Link}}/parent/add" id="addParentForm">
{{$.CsrfTokenHtml}}
<div class="ui fluid action input">
<div class="ui search selection dropdown" id="new-parent-drop-list" data-issue-id="{{.Issue.ID}}">
<input name="newParent" type="hidden">
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<input type="text" class="search">
<div class="default text">{{.i18n.Tr "repo.issues.parent.add"}}</div>
</div>
<button class="ui green icon button">
{{svg "octicon-plus"}}
</button>
</div>
</form>
</div>
{{end}}
</div>
{{if and .CanCreateIssueParents (not .Repository.IsArchived)}}
<input type="hidden" id="crossRepoSearch" value="{{.AllowCrossRepositoryParents}}">
<div class="ui basic modal remove-parent">
<div class="ui icon header">
{{svg "octicon-trash"}}
{{.i18n.Tr "repo.issues.parent.remove_header"}}
</div>
<div class="content">
<form method="POST" action="{{.Issue.Link}}/parent/delete" id="removeParentForm">
{{$.CsrfTokenHtml}}
<input type="hidden" value="" name="removeParentID" id="removeParentID"/>
<input type="hidden" value="" name="parentType" id="parentType"/>
</form>
<p>{{if .Issue.IsPull}}
{{.i18n.Tr "repo.issues.parent.pr_remove_text"}}
{{else}}
{{.i18n.Tr "repo.issues.parent.issue_remove_text"}}
{{end}}</p>
</div>
<div class="actions">
<div class="ui red cancel inverted button">
{{svg "octicon-x"}}
{{.i18n.Tr "repo.issues.parent.cancel"}}
</div>
<div class="ui green ok inverted button">
{{svg "octicon-check"}}
{{.i18n.Tr "repo.issues.parent.remove"}}
</div>
</div>
</div>
{{end}}
{{end}}
{{if .Repository.IsDependenciesEnabled}}
<div class="ui divider"></div>

6
templates/repo/settings/options.tmpl

@ -329,6 +329,12 @@
<label>{{.i18n.Tr "repo.issues.dependency.setting"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input name="enable_issue_parents" type="checkbox" {{if (.Repository.IsParentsEnabled)}}checked{{end}}>
<label>{{.i18n.Tr "repo.issues.parent.setting"}}</label>
</div>
</div>
<div class="ui checkbox">
<input name="enable_close_issues_via_commit_in_any_branch" type="checkbox" {{ if .Repository.CloseIssuesViaCommitInAnyBranch }}checked{{end}}>
<label>{{.i18n.Tr "repo.settings.admin_enable_close_issues_via_commit_in_any_branch"}}</label>

5
templates/swagger/v1_json.tmpl

@ -15592,6 +15592,11 @@
"type": "boolean",
"x-go-name": "EnableIssueDependencies"
},
"enable_issue_parents": {
"description": "Enable parents for issues and pull requests (Built-in issue tracker)",
"type": "boolean",
"x-go-name": "EnableIssueParents"
},
"enable_time_tracker": {
"description": "Enable time tracking (Built-in issue tracker)",
"type": "boolean",

46
web_src/js/features/repo-issue.js

@ -121,6 +121,34 @@ export function initRepoIssueList() {
fullTextSearch: true,
});
$('#new-parent-drop-list')
.dropdown({
apiSettings: {
url: issueSearchUrl,
onResponse(response) {
const filteredResponse = {success: true, results: []};
const currIssueId = $('#new-parent-drop-list').data('issue-id');
// Parse the response from the api to work with our dropdown
$.each(response, (_i, issue) => {
// Don't list current issue in the parent list.
if (issue.id === currIssueId) {
return;
}
filteredResponse.results.push({
name: `#${issue.number} ${htmlEscape(issue.title)
}<div class="text small dont-break-out">${htmlEscape(issue.repository.full_name)}</div>`,
value: issue.id,
});
});
return filteredResponse;
},
cache: false,
},
fullTextSearch: true,
});
function excludeLabel(item) {
const href = $(item).attr('href');
const id = $(item).data('label-id');
@ -196,6 +224,24 @@ export function initRepoIssueDependencyDelete() {
});
}
export function initRepoIssueParentDelete() {
// Delete Issue parent
$(document).on('click', '.delete-parent-button', (e) => {
const id = e.currentTarget.getAttribute('data-id');
const type = e.currentTarget.getAttribute('data-type');
$('.remove-parent').modal({
closable: false,
duration: 200,
onApprove: () => {
$('#removeParentID').val(id);
$('#parentType').val(type);
$('#removeParentForm').trigger('submit');
},
}).modal('show');
});
}
export function initRepoIssueCodeCommentCancel() {
// Cancel inline code comment
$(document).on('click', '.cancel-code-comment', (e) => {

3
web_src/js/features/repo-legacy.js

@ -4,7 +4,7 @@ import {initCompImagePaste, initEasyMDEImagePaste} from './comp/ImagePaste.js';
import {
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel,
initRepoIssueCommentDelete,
initRepoIssueComments, initRepoIssueDependencyDelete,
initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueParentDelete,
initRepoIssueReferenceIssue, initRepoIssueStatusButton,
initRepoIssueTitleEdit,
initRepoIssueWipToggle, initRepoPullRequestMerge, initRepoPullRequestUpdate,
@ -516,6 +516,7 @@ export function initRepository() {
initRepoIssueCommentDelete();
initRepoIssueDependencyDelete();
initRepoIssueParentDelete();
initRepoIssueCodeCommentCancel();
initRepoIssueStatusButton();
initRepoPullRequestMerge();

15
web_src/less/_repository.less

@ -2929,6 +2929,21 @@ tbody.commit-list {
}
}
#new-parent-drop-list {
&.ui.selection.dropdown {
min-width: 0;
width: 100%;
border-radius: 4px 0 0 4px;
border-right: 0;
white-space: nowrap;
}
.text {
width: 100%;
overflow: hidden;
}
}
#manage_topic {
font-size: 12px;
}

Loading…
Cancel
Save