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