mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-25 08:59:31 -05:00
parent
7be5935c55
commit
1bff02de55
29 changed files with 967 additions and 48 deletions
|
@ -316,6 +316,9 @@ DEFAULT_KEEP_EMAIL_PRIVATE = false
|
||||||
; Default value for AllowCreateOrganization
|
; Default value for AllowCreateOrganization
|
||||||
; Every new user will have rights set to create organizations depending on this setting
|
; Every new user will have rights set to create organizations depending on this setting
|
||||||
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
|
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
|
||||||
|
; Default value for EnableDependencies
|
||||||
|
; Repositories will use depencies by default depending on this setting
|
||||||
|
DEFAULT_ENABLE_DEPENDENCIES = true
|
||||||
; Enable Timetracking
|
; Enable Timetracking
|
||||||
ENABLE_TIMETRACKING = true
|
ENABLE_TIMETRACKING = true
|
||||||
; Default value for EnableTimetracking
|
; Default value for EnableTimetracking
|
||||||
|
|
|
@ -182,6 +182,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
|
||||||
- `CAPTCHA_TYPE`: **image**: \[image, recaptcha\]
|
- `CAPTCHA_TYPE`: **image**: \[image, recaptcha\]
|
||||||
- `RECAPTCHA_SECRET`: **""**: Go to https://www.google.com/recaptcha/admin to get a secret for recaptcha
|
- `RECAPTCHA_SECRET`: **""**: Go to https://www.google.com/recaptcha/admin to get a secret for recaptcha
|
||||||
- `RECAPTCHA_SITEKEY`: **""**: Go to https://www.google.com/recaptcha/admin to get a sitekey for recaptcha
|
- `RECAPTCHA_SITEKEY`: **""**: Go to https://www.google.com/recaptcha/admin to get a sitekey for recaptcha
|
||||||
|
- `DEFAULT_ENABLE_DEPENDENCIES`: **true** Enable this to have dependencies enabled by default.
|
||||||
|
|
||||||
## Webhook (`webhook`)
|
## Webhook (`webhook`)
|
||||||
|
|
||||||
|
|
|
@ -477,6 +477,10 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = issue.ChangeStatus(doer, repo, true); err != nil {
|
if err = issue.ChangeStatus(doer, repo, true); err != nil {
|
||||||
|
// Don't return an error when dependencies are open as this would let the push fail
|
||||||
|
if IsErrDependenciesLeft(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1259,3 +1259,88 @@ func IsErrU2FRegistrationNotExist(err error) bool {
|
||||||
_, ok := err.(ErrU2FRegistrationNotExist)
|
_, ok := err.(ErrU2FRegistrationNotExist)
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// .___ ________ .___ .__
|
||||||
|
// | | ______ ________ __ ____ \______ \ ____ ______ ____ ____ __| _/____ ____ ____ |__| ____ ______
|
||||||
|
// | |/ ___// ___/ | \_/ __ \ | | \_/ __ \\____ \_/ __ \ / \ / __ |/ __ \ / \_/ ___\| |/ __ \ / ___/
|
||||||
|
// | |\___ \ \___ \| | /\ ___/ | ` \ ___/| |_> > ___/| | \/ /_/ \ ___/| | \ \___| \ ___/ \___ \
|
||||||
|
// |___/____ >____ >____/ \___ >_______ /\___ > __/ \___ >___| /\____ |\___ >___| /\___ >__|\___ >____ >
|
||||||
|
// \/ \/ \/ \/ \/|__| \/ \/ \/ \/ \/ \/ \/ \/
|
||||||
|
|
||||||
|
// ErrDependencyExists represents a "DependencyAlreadyExists" kind of error.
|
||||||
|
type ErrDependencyExists struct {
|
||||||
|
IssueID int64
|
||||||
|
DependencyID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrDependencyExists checks if an error is a ErrDependencyExists.
|
||||||
|
func IsErrDependencyExists(err error) bool {
|
||||||
|
_, ok := err.(ErrDependencyExists)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrDependencyExists) Error() string {
|
||||||
|
return fmt.Sprintf("issue dependency does already exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrDependencyNotExists represents a "DependencyAlreadyExists" kind of error.
|
||||||
|
type ErrDependencyNotExists struct {
|
||||||
|
IssueID int64
|
||||||
|
DependencyID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrDependencyNotExists checks if an error is a ErrDependencyExists.
|
||||||
|
func IsErrDependencyNotExists(err error) bool {
|
||||||
|
_, ok := err.(ErrDependencyNotExists)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrDependencyNotExists) Error() string {
|
||||||
|
return fmt.Sprintf("issue dependency does not exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrCircularDependency represents a "DependencyCircular" kind of error.
|
||||||
|
type ErrCircularDependency struct {
|
||||||
|
IssueID int64
|
||||||
|
DependencyID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrCircularDependency checks if an error is a ErrCircularDependency.
|
||||||
|
func IsErrCircularDependency(err error) bool {
|
||||||
|
_, ok := err.(ErrCircularDependency)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrCircularDependency) Error() string {
|
||||||
|
return fmt.Sprintf("circular dependencies exists (two issues blocking each other) [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrDependenciesLeft represents an error where the issue you're trying to close still has dependencies left.
|
||||||
|
type ErrDependenciesLeft struct {
|
||||||
|
IssueID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrDependenciesLeft checks if an error is a ErrDependenciesLeft.
|
||||||
|
func IsErrDependenciesLeft(err error) bool {
|
||||||
|
_, ok := err.(ErrDependenciesLeft)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrDependenciesLeft) Error() string {
|
||||||
|
return fmt.Sprintf("issue has open dependencies [issue id: %d]", err.IssueID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnknownDependencyType represents an error where an unknown dependency type was passed
|
||||||
|
type ErrUnknownDependencyType struct {
|
||||||
|
Type DependencyType
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrUnknownDependencyType checks if an error is ErrUnknownDependencyType
|
||||||
|
func IsErrUnknownDependencyType(err error) bool {
|
||||||
|
_, ok := err.(ErrUnknownDependencyType)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrUnknownDependencyType) Error() string {
|
||||||
|
return fmt.Sprintf("unknown dependency type [type: %d]", err.Type)
|
||||||
|
}
|
||||||
|
|
|
@ -649,6 +649,20 @@ func (issue *Issue) changeStatus(e *xorm.Session, doer *User, repo *Repository,
|
||||||
if issue.IsClosed == isClosed {
|
if issue.IsClosed == isClosed {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for open dependencies
|
||||||
|
if isClosed && issue.Repo.IsDependenciesEnabled() {
|
||||||
|
// only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies
|
||||||
|
noDeps, err := IssueNoDependenciesLeft(issue)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !noDeps {
|
||||||
|
return ErrDependenciesLeft{issue.ID}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
issue.IsClosed = isClosed
|
issue.IsClosed = isClosed
|
||||||
if isClosed {
|
if isClosed {
|
||||||
issue.ClosedUnix = util.TimeStampNow()
|
issue.ClosedUnix = util.TimeStampNow()
|
||||||
|
@ -1598,3 +1612,33 @@ func UpdateIssueDeadline(issue *Issue, deadlineUnix util.TimeStamp, doer *User)
|
||||||
|
|
||||||
return sess.Commit()
|
return sess.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get Blocked By Dependencies, aka all issues this issue is blocked by.
|
||||||
|
func (issue *Issue) getBlockedByDependencies(e Engine) (issueDeps []*Issue, err error) {
|
||||||
|
return issueDeps, e.
|
||||||
|
Table("issue_dependency").
|
||||||
|
Select("issue.*").
|
||||||
|
Join("INNER", "issue", "issue.id = issue_dependency.dependency_id").
|
||||||
|
Where("issue_id = ?", issue.ID).
|
||||||
|
Find(&issueDeps)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Blocking Dependencies, aka all issues this issue blocks.
|
||||||
|
func (issue *Issue) getBlockingDependencies(e Engine) (issueDeps []*Issue, err error) {
|
||||||
|
return issueDeps, e.
|
||||||
|
Table("issue_dependency").
|
||||||
|
Select("issue.*").
|
||||||
|
Join("INNER", "issue", "issue.id = issue_dependency.issue_id").
|
||||||
|
Where("dependency_id = ?", issue.ID).
|
||||||
|
Find(&issueDeps)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockedByDependencies finds all Dependencies an issue is blocked by
|
||||||
|
func (issue *Issue) BlockedByDependencies() ([]*Issue, error) {
|
||||||
|
return issue.getBlockedByDependencies(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockingDependencies returns all blocking dependencies, aka all other issues a given issue blocks
|
||||||
|
func (issue *Issue) BlockingDependencies() ([]*Issue, error) {
|
||||||
|
return issue.getBlockingDependencies(x)
|
||||||
|
}
|
||||||
|
|
|
@ -66,6 +66,10 @@ const (
|
||||||
CommentTypeModifiedDeadline
|
CommentTypeModifiedDeadline
|
||||||
// Removed a due date
|
// Removed a due date
|
||||||
CommentTypeRemovedDeadline
|
CommentTypeRemovedDeadline
|
||||||
|
// Dependency added
|
||||||
|
CommentTypeAddDependency
|
||||||
|
//Dependency removed
|
||||||
|
CommentTypeRemoveDependency
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommentTag defines comment tag type
|
// CommentTag defines comment tag type
|
||||||
|
@ -98,6 +102,8 @@ type Comment struct {
|
||||||
Assignee *User `xorm:"-"`
|
Assignee *User `xorm:"-"`
|
||||||
OldTitle string
|
OldTitle string
|
||||||
NewTitle string
|
NewTitle string
|
||||||
|
DependentIssueID int64
|
||||||
|
DependentIssue *Issue `xorm:"-"`
|
||||||
|
|
||||||
CommitID int64
|
CommitID int64
|
||||||
Line int64
|
Line int64
|
||||||
|
@ -281,6 +287,15 @@ func (c *Comment) LoadAssigneeUser() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadDepIssueDetails loads Dependent Issue Details
|
||||||
|
func (c *Comment) LoadDepIssueDetails() (err error) {
|
||||||
|
if c.DependentIssueID <= 0 || c.DependentIssue != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.DependentIssue, err = getIssueByID(x, c.DependentIssueID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// MailParticipants sends new comment emails to repository watchers
|
// MailParticipants sends new comment emails to repository watchers
|
||||||
// and mentioned people.
|
// and mentioned people.
|
||||||
func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (err error) {
|
func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (err error) {
|
||||||
|
@ -332,6 +347,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
|
||||||
if opts.Label != nil {
|
if opts.Label != nil {
|
||||||
LabelID = opts.Label.ID
|
LabelID = opts.Label.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
comment := &Comment{
|
comment := &Comment{
|
||||||
Type: opts.Type,
|
Type: opts.Type,
|
||||||
PosterID: opts.Doer.ID,
|
PosterID: opts.Doer.ID,
|
||||||
|
@ -348,6 +364,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
|
||||||
Content: opts.Content,
|
Content: opts.Content,
|
||||||
OldTitle: opts.OldTitle,
|
OldTitle: opts.OldTitle,
|
||||||
NewTitle: opts.NewTitle,
|
NewTitle: opts.NewTitle,
|
||||||
|
DependentIssueID: opts.DependentIssueID,
|
||||||
}
|
}
|
||||||
if _, err = e.Insert(comment); err != nil {
|
if _, err = e.Insert(comment); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -549,6 +566,39 @@ func createDeleteBranchComment(e *xorm.Session, doer *User, repo *Repository, is
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Creates issue dependency comment
|
||||||
|
func createIssueDependencyComment(e *xorm.Session, doer *User, issue *Issue, dependentIssue *Issue, add bool) (err error) {
|
||||||
|
cType := CommentTypeAddDependency
|
||||||
|
if !add {
|
||||||
|
cType = CommentTypeRemoveDependency
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make two comments, one in each issue
|
||||||
|
_, err = createComment(e, &CreateCommentOptions{
|
||||||
|
Type: cType,
|
||||||
|
Doer: doer,
|
||||||
|
Repo: issue.Repo,
|
||||||
|
Issue: issue,
|
||||||
|
DependentIssueID: dependentIssue.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = createComment(e, &CreateCommentOptions{
|
||||||
|
Type: cType,
|
||||||
|
Doer: doer,
|
||||||
|
Repo: issue.Repo,
|
||||||
|
Issue: dependentIssue,
|
||||||
|
DependentIssueID: issue.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// CreateCommentOptions defines options for creating comment
|
// CreateCommentOptions defines options for creating comment
|
||||||
type CreateCommentOptions struct {
|
type CreateCommentOptions struct {
|
||||||
Type CommentType
|
Type CommentType
|
||||||
|
@ -557,6 +607,7 @@ type CreateCommentOptions struct {
|
||||||
Issue *Issue
|
Issue *Issue
|
||||||
Label *Label
|
Label *Label
|
||||||
|
|
||||||
|
DependentIssueID int64
|
||||||
OldMilestoneID int64
|
OldMilestoneID int64
|
||||||
MilestoneID int64
|
MilestoneID int64
|
||||||
AssigneeID int64
|
AssigneeID int64
|
||||||
|
|
137
models/issue_dependency.go
Normal file
137
models/issue_dependency.go
Normal file
|
@ -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
|
||||||
|
}
|
57
models/issue_dependency_test.go
Normal file
57
models/issue_dependency_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -192,6 +192,8 @@ var migrations = []Migration{
|
||||||
NewMigration("Reformat and remove incorrect topics", reformatAndRemoveIncorrectTopics),
|
NewMigration("Reformat and remove incorrect topics", reformatAndRemoveIncorrectTopics),
|
||||||
// v69 -> v70
|
// v69 -> v70
|
||||||
NewMigration("move team units to team_unit table", moveTeamUnitsToTeamUnitTable),
|
NewMigration("move team units to team_unit table", moveTeamUnitsToTeamUnitTable),
|
||||||
|
// v70 -> v71
|
||||||
|
NewMigration("add issue_dependencies", addIssueDependencies),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate database to current version
|
// Migrate database to current version
|
||||||
|
|
100
models/migrations/v70.go
Normal file
100
models/migrations/v70.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -118,6 +118,7 @@ func init() {
|
||||||
new(TrackedTime),
|
new(TrackedTime),
|
||||||
new(DeletedBranch),
|
new(DeletedBranch),
|
||||||
new(RepoIndexerStatus),
|
new(RepoIndexerStatus),
|
||||||
|
new(IssueDependency),
|
||||||
new(LFSLock),
|
new(LFSLock),
|
||||||
new(Reaction),
|
new(Reaction),
|
||||||
new(IssueAssignees),
|
new(IssueAssignees),
|
||||||
|
|
|
@ -1345,7 +1345,11 @@ func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err err
|
||||||
units = append(units, RepoUnit{
|
units = append(units, RepoUnit{
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
Type: tp,
|
Type: tp,
|
||||||
Config: &IssuesConfig{EnableTimetracker: setting.Service.DefaultEnableTimetracking, AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime},
|
Config: &IssuesConfig{
|
||||||
|
EnableTimetracker: setting.Service.DefaultEnableTimetracking,
|
||||||
|
AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime,
|
||||||
|
EnableDependencies: setting.Service.DefaultEnableDependencies,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
} else if tp == UnitTypePullRequests {
|
} else if tp == UnitTypePullRequests {
|
||||||
units = append(units, RepoUnit{
|
units = append(units, RepoUnit{
|
||||||
|
|
|
@ -73,6 +73,7 @@ func (cfg *ExternalTrackerConfig) ToDB() ([]byte, error) {
|
||||||
type IssuesConfig struct {
|
type IssuesConfig struct {
|
||||||
EnableTimetracker bool
|
EnableTimetracker bool
|
||||||
AllowOnlyContributorsToTrackTime bool
|
AllowOnlyContributorsToTrackTime bool
|
||||||
|
EnableDependencies bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromDB fills up a IssuesConfig from serialized format.
|
// FromDB fills up a IssuesConfig from serialized format.
|
||||||
|
@ -165,7 +166,6 @@ func (r *RepoUnit) IssuesConfig() *IssuesConfig {
|
||||||
func (r *RepoUnit) ExternalTrackerConfig() *ExternalTrackerConfig {
|
func (r *RepoUnit) ExternalTrackerConfig() *ExternalTrackerConfig {
|
||||||
return r.Config.(*ExternalTrackerConfig)
|
return r.Config.(*ExternalTrackerConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUnitsByRepoID(e Engine, repoID int64) (units []*RepoUnit, err error) {
|
func getUnitsByRepoID(e Engine, repoID int64) (units []*RepoUnit, err error) {
|
||||||
return units, e.Where("repo_id = ?", repoID).Find(&units)
|
return units, e.Where("repo_id = ?", repoID).Find(&units)
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,6 +113,7 @@ type RepoSettingForm struct {
|
||||||
PullsAllowSquash bool
|
PullsAllowSquash bool
|
||||||
EnableTimetracker bool
|
EnableTimetracker bool
|
||||||
AllowOnlyContributorsToTrackTime bool
|
AllowOnlyContributorsToTrackTime bool
|
||||||
|
EnableIssueDependencies bool
|
||||||
|
|
||||||
// Admin settings
|
// Admin settings
|
||||||
EnableHealthCheck bool
|
EnableHealthCheck bool
|
||||||
|
|
|
@ -104,6 +104,11 @@ func (r *Repository) CanUseTimetracker(issue *models.Issue, user *models.User) b
|
||||||
r.IsWriter() || issue.IsPoster(user.ID) || isAssigned)
|
r.IsWriter() || issue.IsPoster(user.ID) || isAssigned)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CanCreateIssueDependencies returns whether or not a user can create dependencies.
|
||||||
|
func (r *Repository) CanCreateIssueDependencies(user *models.User) bool {
|
||||||
|
return r.Repository.IsDependenciesEnabled() && r.IsWriter()
|
||||||
|
}
|
||||||
|
|
||||||
// GetCommitsCount returns cached commit count for current view
|
// GetCommitsCount returns cached commit count for current view
|
||||||
func (r *Repository) GetCommitsCount() (int64, error) {
|
func (r *Repository) GetCommitsCount() (int64, error) {
|
||||||
var contextName string
|
var contextName string
|
||||||
|
|
|
@ -1180,6 +1180,7 @@ var Service struct {
|
||||||
DefaultAllowCreateOrganization bool
|
DefaultAllowCreateOrganization bool
|
||||||
EnableTimetracking bool
|
EnableTimetracking bool
|
||||||
DefaultEnableTimetracking bool
|
DefaultEnableTimetracking bool
|
||||||
|
DefaultEnableDependencies bool
|
||||||
DefaultAllowOnlyContributorsToTrackTime bool
|
DefaultAllowOnlyContributorsToTrackTime bool
|
||||||
NoReplyAddress string
|
NoReplyAddress string
|
||||||
|
|
||||||
|
@ -1210,6 +1211,7 @@ func newService() {
|
||||||
if Service.EnableTimetracking {
|
if Service.EnableTimetracking {
|
||||||
Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true)
|
Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true)
|
||||||
}
|
}
|
||||||
|
Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_DEPENDENCIES").MustBool(true)
|
||||||
Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true)
|
Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true)
|
||||||
Service.NoReplyAddress = sec.Key("NO_REPLY_ADDRESS").MustString("noreply.example.org")
|
Service.NoReplyAddress = sec.Key("NO_REPLY_ADDRESS").MustString("noreply.example.org")
|
||||||
|
|
||||||
|
|
|
@ -782,6 +782,33 @@ issues.due_date_modified = "modified the due date to %s from %s %s"
|
||||||
issues.due_date_remove = "removed the due date %s %s"
|
issues.due_date_remove = "removed the due date %s %s"
|
||||||
issues.due_date_overdue = "Overdue"
|
issues.due_date_overdue = "Overdue"
|
||||||
issues.due_date_invalid = "The due date is invalid or out of range. Please use the format yyyy-mm-dd."
|
issues.due_date_invalid = "The due date is invalid or out of range. Please use the format yyyy-mm-dd."
|
||||||
|
issues.dependency.title = Dependencies
|
||||||
|
issues.dependency.issue_no_dependencies = This issue currently doesn't have any dependencies.
|
||||||
|
issues.dependency.pr_no_dependencies = This pull request currently doesn't have any dependencies.
|
||||||
|
issues.dependency.add = Add a new dependency...
|
||||||
|
issues.dependency.cancel = Cancel
|
||||||
|
issues.dependency.remove = Remove
|
||||||
|
issues.dependency.issue_number = Issuenumber
|
||||||
|
issues.dependency.added_dependency = `<a href="%[1]s">%[2]s</a> added a new dependency %[3]s`
|
||||||
|
issues.dependency.removed_dependency = `<a href="%[1]s">%[2]s</a> removed a dependency %[3]s`
|
||||||
|
issues.dependency.issue_closing_blockedby = Closing this pull request is blocked by the following issues
|
||||||
|
issues.dependency.pr_closing_blockedby = Closing this issue is blocked by the following issues
|
||||||
|
issues.dependency.issue_close_blocks = This issue blocks closing of the following issues
|
||||||
|
issues.dependency.pr_close_blocks = This pull request blocks closing of the following issues
|
||||||
|
issues.dependency.issue_close_blocked = You need to close all issues blocking this issue before you can close it!
|
||||||
|
issues.dependency.pr_close_blocked = You need to close all issues blocking this pull request before you can merge it!
|
||||||
|
issues.dependency.blocks_short = Blocks
|
||||||
|
issues.dependency.blocked_by_short = Depends on
|
||||||
|
issues.dependency.remove_header = Remove Dependency
|
||||||
|
issues.dependency.issue_remove_text = This will remove the dependency to this issue. Are you sure? You cannot undo this!
|
||||||
|
issues.dependency.pr_remove_text = This will remove the dependency to this pull request. Are you sure? You cannot undo this!
|
||||||
|
issues.dependency.setting = Issues & PRs can have dependencies
|
||||||
|
issues.dependency.add_error_same_issue = You cannot make an issue depend on itself!
|
||||||
|
issues.dependency.add_error_dep_issue_not_exist = Dependent issue does not exist!
|
||||||
|
issues.dependency.add_error_dep_not_exist = Dependency does not exist!
|
||||||
|
issues.dependency.add_error_dep_exists = Dependency already exists!
|
||||||
|
issues.dependency.add_error_cannot_create_circular = You cannot create a dependency with two issues blocking each other!
|
||||||
|
issues.dependency.add_error_dep_not_same_repo = Both issues must be in the same repo!
|
||||||
|
|
||||||
pulls.desc = Enable merge requests and code reviews.
|
pulls.desc = Enable merge requests and code reviews.
|
||||||
pulls.new = New Pull Request
|
pulls.new = New Pull Request
|
||||||
|
@ -1500,6 +1527,7 @@ config.enable_timetracking = Enable Time Tracking
|
||||||
config.default_enable_timetracking = Enable Time Tracking by Default
|
config.default_enable_timetracking = Enable Time Tracking by Default
|
||||||
config.default_allow_only_contributors_to_track_time = Let Only Contributors Track Time
|
config.default_allow_only_contributors_to_track_time = Let Only Contributors Track Time
|
||||||
config.no_reply_address = Hidden Email Domain
|
config.no_reply_address = Hidden Email Domain
|
||||||
|
config.default_enable_dependencies = Enable issue dependencies by default
|
||||||
|
|
||||||
config.webhook_config = Webhook Configuration
|
config.webhook_config = Webhook Configuration
|
||||||
config.queue_length = Queue Length
|
config.queue_length = Queue Length
|
||||||
|
|
|
@ -1769,6 +1769,7 @@ $(document).ready(function () {
|
||||||
initTopicbar();
|
initTopicbar();
|
||||||
initU2FAuth();
|
initU2FAuth();
|
||||||
initU2FRegister();
|
initU2FRegister();
|
||||||
|
initIssueList();
|
||||||
|
|
||||||
// Repo clone url.
|
// Repo clone url.
|
||||||
if ($('#repo-clone-url').length > 0) {
|
if ($('#repo-clone-url').length > 0) {
|
||||||
|
@ -2488,3 +2489,41 @@ function updateDeadline(deadlineString) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deleteDependencyModal(id, type) {
|
||||||
|
$('.remove-dependency')
|
||||||
|
.modal({
|
||||||
|
closable: false,
|
||||||
|
duration: 200,
|
||||||
|
onApprove: function () {
|
||||||
|
$('#removeDependencyID').val(id);
|
||||||
|
$('#dependencyType').val(type);
|
||||||
|
$('#removeDependencyForm').submit();
|
||||||
|
}
|
||||||
|
}).modal('show')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initIssueList() {
|
||||||
|
var repolink = $('#repolink').val();
|
||||||
|
$('.new-dependency-drop-list')
|
||||||
|
.dropdown({
|
||||||
|
apiSettings: {
|
||||||
|
url: '/api/v1/repos' + repolink + '/issues?q={query}',
|
||||||
|
onResponse: function(response) {
|
||||||
|
var filteredResponse = {'success': true, 'results': []};
|
||||||
|
// Parse the response from the api to work with our dropdown
|
||||||
|
$.each(response, function(index, issue) {
|
||||||
|
filteredResponse.results.push({
|
||||||
|
'name' : '#' + issue.number + ' ' + issue.title,
|
||||||
|
'value' : issue.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return filteredResponse;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
fullTextSearch: true
|
||||||
|
})
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
|
@ -208,6 +209,10 @@ func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) {
|
||||||
|
|
||||||
if form.Closed {
|
if form.Closed {
|
||||||
if err := issue.ChangeStatus(ctx.User, ctx.Repo.Repository, true); err != nil {
|
if err := issue.ChangeStatus(ctx.User, ctx.Repo.Repository, true); err != nil {
|
||||||
|
if models.IsErrDependenciesLeft(err) {
|
||||||
|
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.Error(500, "ChangeStatus", err)
|
ctx.Error(500, "ChangeStatus", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -325,6 +330,10 @@ func EditIssue(ctx *context.APIContext, form api.EditIssueOption) {
|
||||||
}
|
}
|
||||||
if form.State != nil {
|
if form.State != nil {
|
||||||
if err = issue.ChangeStatus(ctx.User, ctx.Repo.Repository, api.StateClosed == api.StateType(*form.State)); err != nil {
|
if err = issue.ChangeStatus(ctx.User, ctx.Repo.Repository, api.StateClosed == api.StateType(*form.State)); err != nil {
|
||||||
|
if models.IsErrDependenciesLeft(err) {
|
||||||
|
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.Error(500, "ChangeStatus", err)
|
ctx.Error(500, "ChangeStatus", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/git"
|
"code.gitea.io/git"
|
||||||
|
@ -378,6 +379,10 @@ func EditPullRequest(ctx *context.APIContext, form api.EditPullRequestOption) {
|
||||||
}
|
}
|
||||||
if form.State != nil {
|
if form.State != nil {
|
||||||
if err = issue.ChangeStatus(ctx.User, ctx.Repo.Repository, api.StateClosed == api.StateType(*form.State)); err != nil {
|
if err = issue.ChangeStatus(ctx.User, ctx.Repo.Repository, api.StateClosed == api.StateType(*form.State)); err != nil {
|
||||||
|
if models.IsErrDependenciesLeft(err) {
|
||||||
|
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies")
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.Error(500, "ChangeStatus", err)
|
ctx.Error(500, "ChangeStatus", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -302,6 +303,9 @@ func RetrieveRepoMetas(ctx *context.Context, repo *models.Repository) []*models.
|
||||||
}
|
}
|
||||||
ctx.Data["Branches"] = brs
|
ctx.Data["Branches"] = brs
|
||||||
|
|
||||||
|
// Contains true if the user can create issue dependencies
|
||||||
|
ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User)
|
||||||
|
|
||||||
return labels
|
return labels
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -665,6 +669,9 @@ func ViewIssue(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the user can use the dependencies
|
||||||
|
ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User)
|
||||||
|
|
||||||
// Render comments and and fetch participants.
|
// Render comments and and fetch participants.
|
||||||
participants[0] = issue.Poster
|
participants[0] = issue.Poster
|
||||||
for _, comment = range issue.Comments {
|
for _, comment = range issue.Comments {
|
||||||
|
@ -721,6 +728,11 @@ func ViewIssue(ctx *context.Context) {
|
||||||
ctx.ServerError("LoadAssigneeUser", err)
|
ctx.ServerError("LoadAssigneeUser", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} else if comment.Type == models.CommentTypeRemoveDependency || comment.Type == models.CommentTypeAddDependency {
|
||||||
|
if err = comment.LoadDepIssueDetails(); err != nil {
|
||||||
|
ctx.ServerError("LoadDepIssueDetails", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -774,6 +786,10 @@ func ViewIssue(ctx *context.Context) {
|
||||||
ctx.Data["IsPullBranchDeletable"] = canDelete && pull.HeadRepo != nil && git.IsBranchExist(pull.HeadRepo.RepoPath(), pull.HeadBranch)
|
ctx.Data["IsPullBranchDeletable"] = canDelete && pull.HeadRepo != nil && git.IsBranchExist(pull.HeadRepo.RepoPath(), pull.HeadBranch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get Dependencies
|
||||||
|
ctx.Data["BlockedByDependencies"], err = issue.BlockedByDependencies()
|
||||||
|
ctx.Data["BlockingDependencies"], err = issue.BlockingDependencies()
|
||||||
|
|
||||||
ctx.Data["Participants"] = participants
|
ctx.Data["Participants"] = participants
|
||||||
ctx.Data["NumParticipants"] = len(participants)
|
ctx.Data["NumParticipants"] = len(participants)
|
||||||
ctx.Data["Issue"] = issue
|
ctx.Data["Issue"] = issue
|
||||||
|
@ -971,6 +987,12 @@ func UpdateIssueStatus(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
if err := issue.ChangeStatus(ctx.User, issue.Repo, isClosed); err != nil {
|
if err := issue.ChangeStatus(ctx.User, issue.Repo, isClosed); err != nil {
|
||||||
|
if models.IsErrDependenciesLeft(err) {
|
||||||
|
ctx.JSON(http.StatusPreconditionFailed, map[string]interface{}{
|
||||||
|
"error": "cannot close this issue because it still has open dependencies",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.ServerError("ChangeStatus", err)
|
ctx.ServerError("ChangeStatus", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1034,6 +1056,17 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) {
|
||||||
} else {
|
} else {
|
||||||
if err := issue.ChangeStatus(ctx.User, ctx.Repo.Repository, form.Status == "close"); err != nil {
|
if err := issue.ChangeStatus(ctx.User, ctx.Repo.Repository, form.Status == "close"); err != nil {
|
||||||
log.Error(4, "ChangeStatus: %v", err)
|
log.Error(4, "ChangeStatus: %v", err)
|
||||||
|
|
||||||
|
if models.IsErrDependenciesLeft(err) {
|
||||||
|
if issue.IsPull {
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
|
||||||
|
ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
|
||||||
|
} else {
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.issue_close_blocked"))
|
||||||
|
ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
|
log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
|
||||||
|
|
||||||
|
|
119
routers/repo/issue_dependency.go
Normal file
119
routers/repo/issue_dependency.go
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -524,6 +524,18 @@ func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
|
||||||
|
|
||||||
pr.Issue = issue
|
pr.Issue = issue
|
||||||
pr.Issue.Repo = ctx.Repo.Repository
|
pr.Issue.Repo = ctx.Repo.Repository
|
||||||
|
|
||||||
|
noDeps, err := models.IssueNoDependenciesLeft(issue)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !noDeps {
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err = pr.Merge(ctx.User, ctx.Repo.GitRepo, models.MergeStyle(form.Do), message); err != nil {
|
if err = pr.Merge(ctx.User, ctx.Repo.GitRepo, models.MergeStyle(form.Do), message); err != nil {
|
||||||
if models.IsErrInvalidMergeStyle(err) {
|
if models.IsErrInvalidMergeStyle(err) {
|
||||||
ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
|
ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
|
||||||
|
|
|
@ -202,6 +202,7 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
|
||||||
Config: &models.IssuesConfig{
|
Config: &models.IssuesConfig{
|
||||||
EnableTimetracker: form.EnableTimetracker,
|
EnableTimetracker: form.EnableTimetracker,
|
||||||
AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
|
AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
|
||||||
|
EnableDependencies: form.EnableIssueDependencies,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -523,6 +523,10 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
m.Post("/title", repo.UpdateIssueTitle)
|
m.Post("/title", repo.UpdateIssueTitle)
|
||||||
m.Post("/content", repo.UpdateIssueContent)
|
m.Post("/content", repo.UpdateIssueContent)
|
||||||
m.Post("/watch", repo.IssueWatch)
|
m.Post("/watch", repo.IssueWatch)
|
||||||
|
m.Group("/dependency", func() {
|
||||||
|
m.Post("/add", repo.AddDependency)
|
||||||
|
m.Post("/delete", repo.RemoveDependency)
|
||||||
|
})
|
||||||
m.Combo("/comments").Post(bindIgnErr(auth.CreateCommentForm{}), repo.NewComment)
|
m.Combo("/comments").Post(bindIgnErr(auth.CreateCommentForm{}), repo.NewComment)
|
||||||
m.Group("/times", func() {
|
m.Group("/times", func() {
|
||||||
m.Post("/add", bindIgnErr(auth.AddTimeManuallyForm{}), repo.AddTimeManually)
|
m.Post("/add", bindIgnErr(auth.AddTimeManuallyForm{}), repo.AddTimeManually)
|
||||||
|
|
|
@ -150,6 +150,8 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
<dt>{{.i18n.Tr "admin.config.no_reply_address"}}</dt>
|
<dt>{{.i18n.Tr "admin.config.no_reply_address"}}</dt>
|
||||||
<dd>{{if .Service.NoReplyAddress}}{{.Service.NoReplyAddress}}{{else}}-{{end}}</dd>
|
<dd>{{if .Service.NoReplyAddress}}{{.Service.NoReplyAddress}}{{else}}-{{end}}</dd>
|
||||||
|
<dt>{{.i18n.Tr "admin.config.default_enable_dependencies"}}</dt>
|
||||||
|
<dd><i class="fa fa{{if .Service.DefaultEnableDependencies}}-check{{end}}-square-o"></i></dd>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<dt>{{.i18n.Tr "admin.config.active_code_lives"}}</dt>
|
<dt>{{.i18n.Tr "admin.config.active_code_lives"}}</dt>
|
||||||
<dd>{{.Service.ActiveCodeLives}} {{.i18n.Tr "tool.raw_minutes"}}</dd>
|
<dd>{{.Service.ActiveCodeLives}} {{.i18n.Tr "tool.raw_minutes"}}</dd>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{{range .Issue.Comments}}
|
{{range .Issue.Comments}}
|
||||||
{{ $createdStr:= TimeSinceUnix .CreatedUnix $.Lang }}
|
{{ $createdStr:= TimeSinceUnix .CreatedUnix $.Lang }}
|
||||||
|
|
||||||
<!-- 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE_REF, 4 = COMMIT_REF, 5 = COMMENT_REF, 6 = PULL_REF, 7 = COMMENT_LABEL, 12 = START_TRACKING, 13 = STOP_TRACKING, 14 = ADD_TIME_MANUAL, 16 = ADDED_DEADLINE, 17 = MODIFIED_DEADLINE, 18 = REMOVED_DEADLINE -->
|
<!-- 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE_REF, 4 = COMMIT_REF, 5 = COMMENT_REF, 6 = PULL_REF, 7 = COMMENT_LABEL, 12 = START_TRACKING, 13 = STOP_TRACKING, 14 = ADD_TIME_MANUAL, 16 = ADDED_DEADLINE, 17 = MODIFIED_DEADLINE, 18 = REMOVED_DEADLINE, 19 = ADD_DEPENDENCY, 20 = REMOVE_DEPENDENCY -->
|
||||||
{{if eq .Type 0}}
|
{{if eq .Type 0}}
|
||||||
<div class="comment" id="{{.HashTag}}">
|
<div class="comment" id="{{.HashTag}}">
|
||||||
<a class="avatar" {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}>
|
<a class="avatar" {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}>
|
||||||
|
@ -65,7 +65,6 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{else if eq .Type 1}}
|
{{else if eq .Type 1}}
|
||||||
<div class="event">
|
<div class="event">
|
||||||
<span class="octicon octicon-primitive-dot"></span>
|
<span class="octicon octicon-primitive-dot"></span>
|
||||||
|
@ -233,5 +232,33 @@
|
||||||
{{$.i18n.Tr "repo.issues.due_date_remove" .Content $createdStr | Safe}}
|
{{$.i18n.Tr "repo.issues.due_date_remove" .Content $createdStr | Safe}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{{else if eq .Type 19}}
|
||||||
|
<div class="event">
|
||||||
|
<span class="octicon octicon-primitive-dot"></span>
|
||||||
|
<a class="ui avatar image" href="{{.Poster.HomeLink}}">
|
||||||
|
<img src="{{.Poster.RelAvatarLink}}">
|
||||||
|
</a>
|
||||||
|
<span class="text grey">
|
||||||
|
{{$.i18n.Tr "repo.issues.dependency.added_dependency" .Poster.HomeLink .Poster.Name $createdStr | Safe}}
|
||||||
|
</span>
|
||||||
|
<div class="detail">
|
||||||
|
<span class="octicon octicon-plus"></span>
|
||||||
|
<span class="text grey"><a href="{{$.RepoLink}}/issues/{{.DependentIssue.Index}}">#{{.DependentIssue.Index}} {{.DependentIssue.Title}}</a></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else if eq .Type 20}}
|
||||||
|
<div class="event">
|
||||||
|
<span class="octicon octicon-primitive-dot"></span>
|
||||||
|
<a class="ui avatar image" href="{{.Poster.HomeLink}}">
|
||||||
|
<img src="{{.Poster.RelAvatarLink}}">
|
||||||
|
</a>
|
||||||
|
<span class="text grey">
|
||||||
|
{{$.i18n.Tr "repo.issues.dependency.removed_dependency" .Poster.HomeLink .Poster.Name $createdStr | Safe}}
|
||||||
|
</span>
|
||||||
|
<div class="detail">
|
||||||
|
<span class="text grey octicon octicon-trashcan"></span>
|
||||||
|
<span class="text grey"><a href="{{$.RepoLink}}/issues/{{.DependentIssue.Index}}">#{{.DependentIssue.Index}} {{.DependentIssue.Title}}</a></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -249,5 +249,142 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{if .Repository.IsDependenciesEnabled}}
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
|
||||||
|
<div class="ui depending">
|
||||||
|
<span class="text"><strong>{{.i18n.Tr "repo.issues.dependency.title"}}</strong></span>
|
||||||
|
<br>
|
||||||
|
{{if .BlockedByDependencies}}
|
||||||
|
<span class="text" data-tooltip="{{if .Issue.IsPull}}
|
||||||
|
{{.i18n.Tr "repo.issues.dependency.issue_closing_blockedby"}}
|
||||||
|
{{else}}
|
||||||
|
{{.i18n.Tr "repo.issues.dependency.pr_closing_blockedby"}}
|
||||||
|
{{end}}" data-inverted="">
|
||||||
|
{{.i18n.Tr "repo.issues.dependency.blocked_by_short"}}:
|
||||||
|
</span>
|
||||||
|
<div class="ui relaxed divided list">
|
||||||
|
{{range .BlockedByDependencies}}
|
||||||
|
<div class="item">
|
||||||
|
<div class="right floated content">
|
||||||
|
{{if $.CanCreateIssueDependencies}}
|
||||||
|
<a class="delete-dependency-button" onclick="deleteDependencyModal({{.ID}}, 'blockedBy');">
|
||||||
|
<i class="delete icon text red"></i>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
{{if .IsClosed}}
|
||||||
|
<div class="ui red mini label">
|
||||||
|
<i class="octicon octicon-issue-closed"></i>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="ui green mini label">
|
||||||
|
<i class="octicon octicon-issue-opened"></i>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="ui black label">#{{.Index}}</div>
|
||||||
|
<a class="title has-emoji" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title}}</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .BlockingDependencies}}
|
||||||
|
<span class="text" data-tooltip="{{if .Issue.IsPull}}
|
||||||
|
{{.i18n.Tr "repo.issues.dependency.pr_close_blocks"}}
|
||||||
|
{{else}}
|
||||||
|
{{.i18n.Tr "repo.issues.dependency.issue_close_blocks"}}
|
||||||
|
{{end}}" data-inverted="">
|
||||||
|
{{.i18n.Tr "repo.issues.dependency.blocks_short"}}:
|
||||||
|
</span>
|
||||||
|
<div class="ui relaxed divided list">
|
||||||
|
{{range .BlockingDependencies}}
|
||||||
|
<div class="item">
|
||||||
|
<div class="right floated content">
|
||||||
|
{{if $.CanCreateIssueDependencies}}
|
||||||
|
<a class="delete-dependency-button" onclick="deleteDependencyModal({{.ID}}, 'blocking');">
|
||||||
|
<i class="delete icon text red"></i>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
{{if .IsClosed}}
|
||||||
|
<div class="ui red tiny label">
|
||||||
|
<i class="octicon octicon-issue-closed"></i>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="ui green mini label">
|
||||||
|
<i class="octicon octicon-issue-opened"></i>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="ui black label">#{{.Index}}</div>
|
||||||
|
<a class="title has-emoji" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title}}</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if (and (not .BlockedByDependencies) (not .BlockingDependencies))}}
|
||||||
|
<p>{{if .Issue.IsPull}}
|
||||||
|
{{.i18n.Tr "repo.issues.dependency.pr_no_dependencies"}}
|
||||||
|
{{else}}
|
||||||
|
{{.i18n.Tr "repo.issues.dependency.issue_no_dependencies"}}
|
||||||
|
{{end}}</p>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .CanCreateIssueDependencies}}
|
||||||
|
<div>
|
||||||
|
<form method="POST" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/dependency/add" id="addDependencyForm">
|
||||||
|
{{$.CsrfTokenHtml}}
|
||||||
|
<div class="ui fluid action input">
|
||||||
|
<div class="ui search selection dropdown new-dependency-drop-list" style="min-width: 13.9rem;border-radius: 4px 0 0 4px;border-right: 0;white-space: nowrap;">
|
||||||
|
<input name="newDependency" type="hidden">
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
<input type="text" class="search">
|
||||||
|
<div class="default text">{{.i18n.Tr "repo.issues.dependency.add"}}</div>
|
||||||
|
</div>
|
||||||
|
<button class="ui green icon button">
|
||||||
|
<i class="plus icon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{if .CanCreateIssueDependencies}}
|
||||||
|
<input type="hidden" id="repolink" value="{{$.RepoLink}}">
|
||||||
|
<!-- I know, there is probably a better way to do this -->
|
||||||
|
<input type="hidden" id="issueIndex" value="{{.Issue.Index}}"/>
|
||||||
|
|
||||||
|
<div class="ui basic modal remove-dependency">
|
||||||
|
<div class="ui icon header">
|
||||||
|
<i class="trash icon"></i>
|
||||||
|
{{.i18n.Tr "repo.issues.dependency.remove_header"}}
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<form method="POST" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/dependency/delete" id="removeDependencyForm">
|
||||||
|
{{$.CsrfTokenHtml}}
|
||||||
|
<input type="hidden" value="" name="removeDependencyID" id="removeDependencyID"/>
|
||||||
|
<input type="hidden" value="" name="dependencyType" id="dependencyType"/>
|
||||||
|
</form>
|
||||||
|
<p>{{if .Issue.IsPull}}
|
||||||
|
{{.i18n.Tr "repo.issues.dependency.pr_remove_text"}}
|
||||||
|
{{else}}
|
||||||
|
{{.i18n.Tr "repo.issues.dependency.issue_remove_text"}}
|
||||||
|
{{end}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<div class="ui basic red cancel inverted button">
|
||||||
|
<i class="remove icon"></i>
|
||||||
|
{{.i18n.Tr "repo.issues.dependency.cancel"}}
|
||||||
|
</div>
|
||||||
|
<div class="ui basic green ok inverted button">
|
||||||
|
<i class="checkmark icon"></i>
|
||||||
|
{{.i18n.Tr "repo.issues.dependency.remove"}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
|
@ -153,6 +153,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="enable_issue_dependencies" type="checkbox" {{if (.Repository.IsDependenciesEnabled)}}checked{{end}}>
|
||||||
|
<label>{{.i18n.Tr "repo.issues.dependency.setting"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="ui radio checkbox">
|
<div class="ui radio checkbox">
|
||||||
|
|
Loading…
Reference in a new issue