parent
7be5935c55
commit
1bff02de55
@ -0,0 +1,137 @@ |
||||
// 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 ( |
||||
"code.gitea.io/gitea/modules/log" |
||||
"code.gitea.io/gitea/modules/setting" |
||||
"code.gitea.io/gitea/modules/util" |
||||
) |
||||
|
||||
// IssueDependency represents an issue dependency
|
||||
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"` |
||||
CreatedUnix util.TimeStamp `xorm:"created"` |
||||
UpdatedUnix util.TimeStamp `xorm:"updated"` |
||||
} |
||||
|
||||
// DependencyType Defines Dependency Type Constants
|
||||
type DependencyType int |
||||
|
||||
// Define Dependency Types
|
||||
const ( |
||||
DependencyTypeBlockedBy DependencyType = iota |
||||
DependencyTypeBlocking |
||||
) |
||||
|
||||
// CreateIssueDependency creates a new dependency for an issue
|
||||
func CreateIssueDependency(user *User, issue, dep *Issue) error { |
||||
sess := x.NewSession() |
||||
defer sess.Close() |
||||
if err := sess.Begin(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Check if it aleready exists
|
||||
exists, err := issueDepExists(sess, issue.ID, dep.ID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if exists { |
||||
return ErrDependencyExists{issue.ID, dep.ID} |
||||
} |
||||
// And if it would be circular
|
||||
circular, err := issueDepExists(sess, dep.ID, issue.ID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if circular { |
||||
return ErrCircularDependency{issue.ID, dep.ID} |
||||
} |
||||
|
||||
if _, err := sess.Insert(&IssueDependency{ |
||||
UserID: user.ID, |
||||
IssueID: issue.ID, |
||||
DependencyID: dep.ID, |
||||
}); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Add comment referencing the new dependency
|
||||
if err = createIssueDependencyComment(sess, user, issue, dep, true); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return sess.Commit() |
||||
} |
||||
|
||||
// RemoveIssueDependency removes a dependency from an issue
|
||||
func RemoveIssueDependency(user *User, issue *Issue, dep *Issue, depType DependencyType) (err error) { |
||||
sess := x.NewSession() |
||||
defer sess.Close() |
||||
if err = sess.Begin(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
var issueDepToDelete IssueDependency |
||||
|
||||
switch depType { |
||||
case DependencyTypeBlockedBy: |
||||
issueDepToDelete = IssueDependency{IssueID: issue.ID, DependencyID: dep.ID} |
||||
case DependencyTypeBlocking: |
||||
issueDepToDelete = IssueDependency{IssueID: dep.ID, DependencyID: issue.ID} |
||||
default: |
||||
return ErrUnknownDependencyType{depType} |
||||
} |
||||
|
||||
affected, err := sess.Delete(&issueDepToDelete) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// If we deleted nothing, the dependency did not exist
|
||||
if affected <= 0 { |
||||
return ErrDependencyNotExists{issue.ID, dep.ID} |
||||
} |
||||
|
||||
// Add comment referencing the removed dependency
|
||||
if err = createIssueDependencyComment(sess, user, issue, dep, false); err != nil { |
||||
return err |
||||
} |
||||
return sess.Commit() |
||||
} |
||||
|
||||
// Check if the dependency already exists
|
||||
func issueDepExists(e Engine, issueID int64, depID int64) (bool, error) { |
||||
return e.Where("(issue_id = ? AND dependency_id = ?)", issueID, depID).Exist(&IssueDependency{}) |
||||
} |
||||
|
||||
// IssueNoDependenciesLeft checks if issue can be closed
|
||||
func IssueNoDependenciesLeft(issue *Issue) (bool, error) { |
||||
|
||||
exists, err := x. |
||||
Table("issue_dependency"). |
||||
Select("issue.*"). |
||||
Join("INNER", "issue", "issue.id = issue_dependency.dependency_id"). |
||||
Where("issue_dependency.issue_id = ?", issue.ID). |
||||
And("issue.is_closed = ?", "0"). |
||||
Exist(&Issue{}) |
||||
|
||||
return !exists, err |
||||
} |
||||
|
||||
// IsDependenciesEnabled returns if dependecies are enabled and returns the default setting if not set.
|
||||
func (repo *Repository) IsDependenciesEnabled() bool { |
||||
var u *RepoUnit |
||||
var err error |
||||
if u, err = repo.GetUnit(UnitTypeIssues); err != nil { |
||||
log.Trace("%s", err) |
||||
return setting.Service.DefaultEnableDependencies |
||||
} |
||||
return u.IssuesConfig().EnableDependencies |
||||
} |
@ -0,0 +1,57 @@ |
||||
// 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" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestCreateIssueDependency(t *testing.T) { |
||||
// Prepare
|
||||
assert.NoError(t, PrepareTestDatabase()) |
||||
|
||||
user1, err := 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 = CreateIssueDependency(user1, issue1, issue2) |
||||
assert.NoError(t, err) |
||||
|
||||
// Do it again to see if it will check if the dependency already exists
|
||||
err = CreateIssueDependency(user1, issue1, issue2) |
||||
assert.Error(t, err) |
||||
assert.True(t, IsErrDependencyExists(err)) |
||||
|
||||
// Check for circular dependencies
|
||||
err = CreateIssueDependency(user1, issue2, issue1) |
||||
assert.Error(t, err) |
||||
assert.True(t, IsErrCircularDependency(err)) |
||||
|
||||
_ = AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeAddDependency, PosterID: user1.ID, IssueID: issue1.ID}) |
||||
|
||||
// Check if dependencies left is correct
|
||||
left, err := IssueNoDependenciesLeft(issue1) |
||||
assert.NoError(t, err) |
||||
assert.False(t, left) |
||||
|
||||
// Close #2 and check again
|
||||
err = issue2.ChangeStatus(user1, issue2.Repo, true) |
||||
assert.NoError(t, err) |
||||
|
||||
left, err = IssueNoDependenciesLeft(issue1) |
||||
assert.NoError(t, err) |
||||
assert.True(t, left) |
||||
|
||||
// Test removing the dependency
|
||||
err = RemoveIssueDependency(user1, issue1, issue2, DependencyTypeBlockedBy) |
||||
assert.NoError(t, err) |
||||
} |
@ -0,0 +1,100 @@ |
||||
// 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 migrations |
||||
|
||||
import ( |
||||
"fmt" |
||||
"time" |
||||
|
||||
"code.gitea.io/gitea/modules/setting" |
||||
|
||||
"github.com/go-xorm/xorm" |
||||
) |
||||
|
||||
func addIssueDependencies(x *xorm.Engine) (err error) { |
||||
|
||||
type IssueDependency struct { |
||||
ID int64 `xorm:"pk autoincr"` |
||||
UserID int64 `xorm:"NOT NULL"` |
||||
IssueID int64 `xorm:"NOT NULL"` |
||||
DependencyID int64 `xorm:"NOT NULL"` |
||||
Created time.Time `xorm:"-"` |
||||
CreatedUnix int64 `xorm:"created"` |
||||
Updated time.Time `xorm:"-"` |
||||
UpdatedUnix int64 `xorm:"updated"` |
||||
} |
||||
|
||||
if err = x.Sync(new(IssueDependency)); err != nil { |
||||
return fmt.Errorf("Error creating issue_dependency_table column definition: %v", err) |
||||
} |
||||
|
||||
// Update Comment definition
|
||||
// This (copied) struct does only contain fields used by xorm as the only use here is to update the database
|
||||
|
||||
// CommentType defines the comment type
|
||||
type CommentType int |
||||
|
||||
// TimeStamp defines a timestamp
|
||||
type TimeStamp int64 |
||||
|
||||
type Comment struct { |
||||
ID int64 `xorm:"pk autoincr"` |
||||
Type CommentType |
||||
PosterID int64 `xorm:"INDEX"` |
||||
IssueID int64 `xorm:"INDEX"` |
||||
LabelID int64 |
||||
OldMilestoneID int64 |
||||
MilestoneID int64 |
||||
OldAssigneeID int64 |
||||
AssigneeID int64 |
||||
OldTitle string |
||||
NewTitle string |
||||
DependentIssueID int64 |
||||
|
||||
CommitID int64 |
||||
Line int64 |
||||
Content string `xorm:"TEXT"` |
||||
|
||||
CreatedUnix TimeStamp `xorm:"INDEX created"` |
||||
UpdatedUnix TimeStamp `xorm:"INDEX updated"` |
||||
|
||||
// Reference issue in commit message
|
||||
CommitSHA string `xorm:"VARCHAR(40)"` |
||||
} |
||||
|
||||
if err = x.Sync(new(Comment)); err != nil { |
||||
return fmt.Errorf("Error updating issue_comment table column definition: %v", err) |
||||
} |
||||
|
||||
// RepoUnit describes all units of a repository
|
||||
type RepoUnit struct { |
||||
ID int64 |
||||
RepoID int64 `xorm:"INDEX(s)"` |
||||
Type int `xorm:"INDEX(s)"` |
||||
Config map[string]interface{} `xorm:"JSON"` |
||||
CreatedUnix int64 `xorm:"INDEX CREATED"` |
||||
Created time.Time `xorm:"-"` |
||||
} |
||||
|
||||
//Updating existing issue units
|
||||
units := make([]*RepoUnit, 0, 100) |
||||
err = x.Where("`type` = ?", V16UnitTypeIssues).Find(&units) |
||||
if err != nil { |
||||
return fmt.Errorf("Query repo units: %v", err) |
||||
} |
||||
for _, unit := range units { |
||||
if unit.Config == nil { |
||||
unit.Config = make(map[string]interface{}) |
||||
} |
||||
if _, ok := unit.Config["EnableDependencies"]; !ok { |
||||
unit.Config["EnableDependencies"] = setting.Service.DefaultEnableDependencies |
||||
} |
||||
if _, err := x.ID(unit.ID).Cols("config").Update(unit); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return err |
||||
} |
@ -0,0 +1,119 @@ |
||||
// 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 repo |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
|
||||
"code.gitea.io/gitea/models" |
||||
"code.gitea.io/gitea/modules/context" |
||||
) |
||||
|
||||
// AddDependency adds new dependencies
|
||||
func AddDependency(ctx *context.Context) { |
||||
// Check if the Repo is allowed to have dependencies
|
||||
if !ctx.Repo.CanCreateIssueDependencies(ctx.User) { |
||||
ctx.Error(http.StatusForbidden, "CanCreateIssueDependencies") |
||||
return |
||||
} |
||||
|
||||
depID := ctx.QueryInt64("newDependency") |
||||
|
||||
issueIndex := ctx.ParamsInt64("index") |
||||
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex) |
||||
if err != nil { |
||||
ctx.ServerError("GetIssueByIndex", err) |
||||
return |
||||
} |
||||
|
||||
// Redirect
|
||||
defer ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issueIndex), http.StatusSeeOther) |
||||
|
||||
// Dependency
|
||||
dep, err := models.GetIssueByID(depID) |
||||
if err != nil { |
||||
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_issue_not_exist")) |
||||
return |
||||
} |
||||
|
||||
// Check if both issues are in the same repo
|
||||
if issue.RepoID != dep.RepoID { |
||||
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_same_repo")) |
||||
return |
||||
} |
||||
|
||||
// Check if issue and dependency is the same
|
||||
if dep.Index == issueIndex { |
||||
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_same_issue")) |
||||
return |
||||
} |
||||
|
||||
err = models.CreateIssueDependency(ctx.User, issue, dep) |
||||
if err != nil { |
||||
if models.IsErrDependencyExists(err) { |
||||
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_exists")) |
||||
return |
||||
} else if models.IsErrCircularDependency(err) { |
||||
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_cannot_create_circular")) |
||||
return |
||||
} else { |
||||
ctx.ServerError("CreateOrUpdateIssueDependency", err) |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
// RemoveDependency removes the dependency
|
||||
func RemoveDependency(ctx *context.Context) { |
||||
// Check if the Repo is allowed to have dependencies
|
||||
if !ctx.Repo.CanCreateIssueDependencies(ctx.User) { |
||||
ctx.Error(http.StatusForbidden, "CanCreateIssueDependencies") |
||||
return |
||||
} |
||||
|
||||
depID := ctx.QueryInt64("removeDependencyID") |
||||
|
||||
issueIndex := ctx.ParamsInt64("index") |
||||
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex) |
||||
if err != nil { |
||||
ctx.ServerError("GetIssueByIndex", err) |
||||
return |
||||
} |
||||
|
||||
// Redirect
|
||||
ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issueIndex), http.StatusSeeOther) |
||||
|
||||
// Dependency Type
|
||||
depTypeStr := ctx.Req.PostForm.Get("dependencyType") |
||||
|
||||
var depType models.DependencyType |
||||
|
||||
switch depTypeStr { |
||||
case "blockedBy": |
||||
depType = models.DependencyTypeBlockedBy |
||||
case "blocking": |
||||
depType = models.DependencyTypeBlocking |
||||
default: |
||||
ctx.Error(http.StatusBadRequest, "GetDependecyType") |
||||
return |
||||
} |
||||
|
||||
// Dependency
|
||||
dep, err := models.GetIssueByID(depID) |
||||
if err != nil { |
||||
ctx.ServerError("GetIssueByID", err) |
||||
return |
||||
} |
||||
|
||||
if err = models.RemoveIssueDependency(ctx.User, issue, dep, depType); err != nil { |
||||
if models.IsErrDependencyNotExists(err) { |
||||
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_exist")) |
||||
return |
||||
} |
||||
ctx.ServerError("RemoveIssueDependency", err) |
||||
return |
||||
} |
||||
} |
Loading…
Reference in new issue