Алексей Безбородов
3 years ago
31 changed files with 806 additions and 2 deletions
@ -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 |
||||||
|
} |
@ -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) |
||||||
|
} |
@ -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) |
||||||
|
} |
Loading…
Reference in new issue