Skip to content

Commit 1bff02d

Browse files
kolaentetechknowlogick
authored andcommitted
Added dependencies for issues (#2196) (#2531)
1 parent 7be5935 commit 1bff02d

File tree

29 files changed

+967
-48
lines changed

29 files changed

+967
-48
lines changed

custom/conf/app.ini.sample

+3
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,9 @@ DEFAULT_KEEP_EMAIL_PRIVATE = false
316316
; Default value for AllowCreateOrganization
317317
; Every new user will have rights set to create organizations depending on this setting
318318
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
319+
; Default value for EnableDependencies
320+
; Repositories will use depencies by default depending on this setting
321+
DEFAULT_ENABLE_DEPENDENCIES = true
319322
; Enable Timetracking
320323
ENABLE_TIMETRACKING = true
321324
; Default value for EnableTimetracking

docs/content/doc/advanced/config-cheat-sheet.en-us.md

+1
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
182182
- `CAPTCHA_TYPE`: **image**: \[image, recaptcha\]
183183
- `RECAPTCHA_SECRET`: **""**: Go to https://www.google.com/recaptcha/admin to get a secret for recaptcha
184184
- `RECAPTCHA_SITEKEY`: **""**: Go to https://www.google.com/recaptcha/admin to get a sitekey for recaptcha
185+
- `DEFAULT_ENABLE_DEPENDENCIES`: **true** Enable this to have dependencies enabled by default.
185186

186187
## Webhook (`webhook`)
187188

models/action.go

+4
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,10 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) err
477477
}
478478

479479
if err = issue.ChangeStatus(doer, repo, true); err != nil {
480+
// Don't return an error when dependencies are open as this would let the push fail
481+
if IsErrDependenciesLeft(err) {
482+
return nil
483+
}
480484
return err
481485
}
482486
}

models/error.go

+85
Original file line numberDiff line numberDiff line change
@@ -1259,3 +1259,88 @@ func IsErrU2FRegistrationNotExist(err error) bool {
12591259
_, ok := err.(ErrU2FRegistrationNotExist)
12601260
return ok
12611261
}
1262+
1263+
// .___ ________ .___ .__
1264+
// | | ______ ________ __ ____ \______ \ ____ ______ ____ ____ __| _/____ ____ ____ |__| ____ ______
1265+
// | |/ ___// ___/ | \_/ __ \ | | \_/ __ \\____ \_/ __ \ / \ / __ |/ __ \ / \_/ ___\| |/ __ \ / ___/
1266+
// | |\___ \ \___ \| | /\ ___/ | ` \ ___/| |_> > ___/| | \/ /_/ \ ___/| | \ \___| \ ___/ \___ \
1267+
// |___/____ >____ >____/ \___ >_______ /\___ > __/ \___ >___| /\____ |\___ >___| /\___ >__|\___ >____ >
1268+
// \/ \/ \/ \/ \/|__| \/ \/ \/ \/ \/ \/ \/ \/
1269+
1270+
// ErrDependencyExists represents a "DependencyAlreadyExists" kind of error.
1271+
type ErrDependencyExists struct {
1272+
IssueID int64
1273+
DependencyID int64
1274+
}
1275+
1276+
// IsErrDependencyExists checks if an error is a ErrDependencyExists.
1277+
func IsErrDependencyExists(err error) bool {
1278+
_, ok := err.(ErrDependencyExists)
1279+
return ok
1280+
}
1281+
1282+
func (err ErrDependencyExists) Error() string {
1283+
return fmt.Sprintf("issue dependency does already exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
1284+
}
1285+
1286+
// ErrDependencyNotExists represents a "DependencyAlreadyExists" kind of error.
1287+
type ErrDependencyNotExists struct {
1288+
IssueID int64
1289+
DependencyID int64
1290+
}
1291+
1292+
// IsErrDependencyNotExists checks if an error is a ErrDependencyExists.
1293+
func IsErrDependencyNotExists(err error) bool {
1294+
_, ok := err.(ErrDependencyNotExists)
1295+
return ok
1296+
}
1297+
1298+
func (err ErrDependencyNotExists) Error() string {
1299+
return fmt.Sprintf("issue dependency does not exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
1300+
}
1301+
1302+
// ErrCircularDependency represents a "DependencyCircular" kind of error.
1303+
type ErrCircularDependency struct {
1304+
IssueID int64
1305+
DependencyID int64
1306+
}
1307+
1308+
// IsErrCircularDependency checks if an error is a ErrCircularDependency.
1309+
func IsErrCircularDependency(err error) bool {
1310+
_, ok := err.(ErrCircularDependency)
1311+
return ok
1312+
}
1313+
1314+
func (err ErrCircularDependency) Error() string {
1315+
return fmt.Sprintf("circular dependencies exists (two issues blocking each other) [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
1316+
}
1317+
1318+
// ErrDependenciesLeft represents an error where the issue you're trying to close still has dependencies left.
1319+
type ErrDependenciesLeft struct {
1320+
IssueID int64
1321+
}
1322+
1323+
// IsErrDependenciesLeft checks if an error is a ErrDependenciesLeft.
1324+
func IsErrDependenciesLeft(err error) bool {
1325+
_, ok := err.(ErrDependenciesLeft)
1326+
return ok
1327+
}
1328+
1329+
func (err ErrDependenciesLeft) Error() string {
1330+
return fmt.Sprintf("issue has open dependencies [issue id: %d]", err.IssueID)
1331+
}
1332+
1333+
// ErrUnknownDependencyType represents an error where an unknown dependency type was passed
1334+
type ErrUnknownDependencyType struct {
1335+
Type DependencyType
1336+
}
1337+
1338+
// IsErrUnknownDependencyType checks if an error is ErrUnknownDependencyType
1339+
func IsErrUnknownDependencyType(err error) bool {
1340+
_, ok := err.(ErrUnknownDependencyType)
1341+
return ok
1342+
}
1343+
1344+
func (err ErrUnknownDependencyType) Error() string {
1345+
return fmt.Sprintf("unknown dependency type [type: %d]", err.Type)
1346+
}

models/issue.go

+44
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,20 @@ func (issue *Issue) changeStatus(e *xorm.Session, doer *User, repo *Repository,
649649
if issue.IsClosed == isClosed {
650650
return nil
651651
}
652+
653+
// Check for open dependencies
654+
if isClosed && issue.Repo.IsDependenciesEnabled() {
655+
// 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
656+
noDeps, err := IssueNoDependenciesLeft(issue)
657+
if err != nil {
658+
return err
659+
}
660+
661+
if !noDeps {
662+
return ErrDependenciesLeft{issue.ID}
663+
}
664+
}
665+
652666
issue.IsClosed = isClosed
653667
if isClosed {
654668
issue.ClosedUnix = util.TimeStampNow()
@@ -1598,3 +1612,33 @@ func UpdateIssueDeadline(issue *Issue, deadlineUnix util.TimeStamp, doer *User)
15981612

15991613
return sess.Commit()
16001614
}
1615+
1616+
// Get Blocked By Dependencies, aka all issues this issue is blocked by.
1617+
func (issue *Issue) getBlockedByDependencies(e Engine) (issueDeps []*Issue, err error) {
1618+
return issueDeps, e.
1619+
Table("issue_dependency").
1620+
Select("issue.*").
1621+
Join("INNER", "issue", "issue.id = issue_dependency.dependency_id").
1622+
Where("issue_id = ?", issue.ID).
1623+
Find(&issueDeps)
1624+
}
1625+
1626+
// Get Blocking Dependencies, aka all issues this issue blocks.
1627+
func (issue *Issue) getBlockingDependencies(e Engine) (issueDeps []*Issue, err error) {
1628+
return issueDeps, e.
1629+
Table("issue_dependency").
1630+
Select("issue.*").
1631+
Join("INNER", "issue", "issue.id = issue_dependency.issue_id").
1632+
Where("dependency_id = ?", issue.ID).
1633+
Find(&issueDeps)
1634+
}
1635+
1636+
// BlockedByDependencies finds all Dependencies an issue is blocked by
1637+
func (issue *Issue) BlockedByDependencies() ([]*Issue, error) {
1638+
return issue.getBlockedByDependencies(x)
1639+
}
1640+
1641+
// BlockingDependencies returns all blocking dependencies, aka all other issues a given issue blocks
1642+
func (issue *Issue) BlockingDependencies() ([]*Issue, error) {
1643+
return issue.getBlockingDependencies(x)
1644+
}

models/issue_comment.go

+94-43
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ const (
6666
CommentTypeModifiedDeadline
6767
// Removed a due date
6868
CommentTypeRemovedDeadline
69+
// Dependency added
70+
CommentTypeAddDependency
71+
//Dependency removed
72+
CommentTypeRemoveDependency
6973
)
7074

7175
// CommentTag defines comment tag type
@@ -81,23 +85,25 @@ const (
8185

8286
// Comment represents a comment in commit and issue page.
8387
type Comment struct {
84-
ID int64 `xorm:"pk autoincr"`
85-
Type CommentType
86-
PosterID int64 `xorm:"INDEX"`
87-
Poster *User `xorm:"-"`
88-
IssueID int64 `xorm:"INDEX"`
89-
Issue *Issue `xorm:"-"`
90-
LabelID int64
91-
Label *Label `xorm:"-"`
92-
OldMilestoneID int64
93-
MilestoneID int64
94-
OldMilestone *Milestone `xorm:"-"`
95-
Milestone *Milestone `xorm:"-"`
96-
AssigneeID int64
97-
RemovedAssignee bool
98-
Assignee *User `xorm:"-"`
99-
OldTitle string
100-
NewTitle string
88+
ID int64 `xorm:"pk autoincr"`
89+
Type CommentType
90+
PosterID int64 `xorm:"INDEX"`
91+
Poster *User `xorm:"-"`
92+
IssueID int64 `xorm:"INDEX"`
93+
Issue *Issue `xorm:"-"`
94+
LabelID int64
95+
Label *Label `xorm:"-"`
96+
OldMilestoneID int64
97+
MilestoneID int64
98+
OldMilestone *Milestone `xorm:"-"`
99+
Milestone *Milestone `xorm:"-"`
100+
AssigneeID int64
101+
RemovedAssignee bool
102+
Assignee *User `xorm:"-"`
103+
OldTitle string
104+
NewTitle string
105+
DependentIssueID int64
106+
DependentIssue *Issue `xorm:"-"`
101107

102108
CommitID int64
103109
Line int64
@@ -281,6 +287,15 @@ func (c *Comment) LoadAssigneeUser() error {
281287
return nil
282288
}
283289

290+
// LoadDepIssueDetails loads Dependent Issue Details
291+
func (c *Comment) LoadDepIssueDetails() (err error) {
292+
if c.DependentIssueID <= 0 || c.DependentIssue != nil {
293+
return nil
294+
}
295+
c.DependentIssue, err = getIssueByID(x, c.DependentIssueID)
296+
return err
297+
}
298+
284299
// MailParticipants sends new comment emails to repository watchers
285300
// and mentioned people.
286301
func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (err error) {
@@ -332,22 +347,24 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
332347
if opts.Label != nil {
333348
LabelID = opts.Label.ID
334349
}
350+
335351
comment := &Comment{
336-
Type: opts.Type,
337-
PosterID: opts.Doer.ID,
338-
Poster: opts.Doer,
339-
IssueID: opts.Issue.ID,
340-
LabelID: LabelID,
341-
OldMilestoneID: opts.OldMilestoneID,
342-
MilestoneID: opts.MilestoneID,
343-
RemovedAssignee: opts.RemovedAssignee,
344-
AssigneeID: opts.AssigneeID,
345-
CommitID: opts.CommitID,
346-
CommitSHA: opts.CommitSHA,
347-
Line: opts.LineNum,
348-
Content: opts.Content,
349-
OldTitle: opts.OldTitle,
350-
NewTitle: opts.NewTitle,
352+
Type: opts.Type,
353+
PosterID: opts.Doer.ID,
354+
Poster: opts.Doer,
355+
IssueID: opts.Issue.ID,
356+
LabelID: LabelID,
357+
OldMilestoneID: opts.OldMilestoneID,
358+
MilestoneID: opts.MilestoneID,
359+
RemovedAssignee: opts.RemovedAssignee,
360+
AssigneeID: opts.AssigneeID,
361+
CommitID: opts.CommitID,
362+
CommitSHA: opts.CommitSHA,
363+
Line: opts.LineNum,
364+
Content: opts.Content,
365+
OldTitle: opts.OldTitle,
366+
NewTitle: opts.NewTitle,
367+
DependentIssueID: opts.DependentIssueID,
351368
}
352369
if _, err = e.Insert(comment); err != nil {
353370
return nil, err
@@ -549,6 +566,39 @@ func createDeleteBranchComment(e *xorm.Session, doer *User, repo *Repository, is
549566
})
550567
}
551568

569+
// Creates issue dependency comment
570+
func createIssueDependencyComment(e *xorm.Session, doer *User, issue *Issue, dependentIssue *Issue, add bool) (err error) {
571+
cType := CommentTypeAddDependency
572+
if !add {
573+
cType = CommentTypeRemoveDependency
574+
}
575+
576+
// Make two comments, one in each issue
577+
_, err = createComment(e, &CreateCommentOptions{
578+
Type: cType,
579+
Doer: doer,
580+
Repo: issue.Repo,
581+
Issue: issue,
582+
DependentIssueID: dependentIssue.ID,
583+
})
584+
if err != nil {
585+
return
586+
}
587+
588+
_, err = createComment(e, &CreateCommentOptions{
589+
Type: cType,
590+
Doer: doer,
591+
Repo: issue.Repo,
592+
Issue: dependentIssue,
593+
DependentIssueID: issue.ID,
594+
})
595+
if err != nil {
596+
return
597+
}
598+
599+
return
600+
}
601+
552602
// CreateCommentOptions defines options for creating comment
553603
type CreateCommentOptions struct {
554604
Type CommentType
@@ -557,17 +607,18 @@ type CreateCommentOptions struct {
557607
Issue *Issue
558608
Label *Label
559609

560-
OldMilestoneID int64
561-
MilestoneID int64
562-
AssigneeID int64
563-
RemovedAssignee bool
564-
OldTitle string
565-
NewTitle string
566-
CommitID int64
567-
CommitSHA string
568-
LineNum int64
569-
Content string
570-
Attachments []string // UUIDs of attachments
610+
DependentIssueID int64
611+
OldMilestoneID int64
612+
MilestoneID int64
613+
AssigneeID int64
614+
RemovedAssignee bool
615+
OldTitle string
616+
NewTitle string
617+
CommitID int64
618+
CommitSHA string
619+
LineNum int64
620+
Content string
621+
Attachments []string // UUIDs of attachments
571622
}
572623

573624
// CreateComment creates comment of issue or commit.

0 commit comments

Comments
 (0)