Алексей Безбородов
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