-
-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Added multi-project feature #34386
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Added multi-project feature #34386
Changes from all commits
d48061b
3a2ffcb
a82a39d
18f2d46
f839864
e75c510
1f43f8a
4f2532d
6a272eb
be48c8b
0365eec
ef8fa68
f5717d0
41be926
cce417c
e912b7e
4c72c32
ec78073
388533f
d132f84
967a233
7e6750e
5c1bc2d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,28 +13,22 @@ import ( | |
) | ||
|
||
// LoadProject load the project the issue was assigned to | ||
func (issue *Issue) LoadProject(ctx context.Context) (err error) { | ||
if issue.Project == nil { | ||
var p project_model.Project | ||
has, err := db.GetEngine(ctx).Table("project"). | ||
func (issue *Issue) LoadProjects(ctx context.Context) (err error) { | ||
if len(issue.Projects) == 0 { | ||
err = db.GetEngine(ctx).Table("project"). | ||
Join("INNER", "project_issue", "project.id=project_issue.project_id"). | ||
Where("project_issue.issue_id = ?", issue.ID).Get(&p) | ||
if err != nil { | ||
return err | ||
} else if has { | ||
issue.Project = &p | ||
} | ||
Where("project_issue.issue_id = ?", issue.ID).Find(&issue.Projects) | ||
} | ||
return err | ||
} | ||
|
||
func (issue *Issue) projectID(ctx context.Context) int64 { | ||
var ip project_model.ProjectIssue | ||
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip) | ||
if err != nil || !has { | ||
return 0 | ||
func (issue *Issue) projectIDs(ctx context.Context) []int64 { | ||
var ids []int64 | ||
if err := db.GetEngine(ctx).Table("project_issue").Where("issue_id=?", issue.ID).Select("project_id").Find(&ids); err != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not safe to ignore the err, in case the SQL would be wrong. |
||
return nil | ||
} | ||
return ip.ProjectID | ||
|
||
return ids | ||
} | ||
|
||
// ProjectColumnID return project column id if issue was assigned to one | ||
|
@@ -68,7 +62,7 @@ func LoadProjectIssueColumnMap(ctx context.Context, projectID, defaultColumnID i | |
func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *IssuesOptions) (IssueList, error) { | ||
issueList, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) { | ||
o.ProjectColumnID = b.ID | ||
o.ProjectID = b.ProjectID | ||
o.ProjectIDs = []int64{b.ProjectID} | ||
o.SortType = "project-column-sorting" | ||
})) | ||
if err != nil { | ||
|
@@ -78,7 +72,7 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is | |
if b.Default { | ||
issues, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) { | ||
o.ProjectColumnID = db.NoConditionID | ||
o.ProjectID = b.ProjectID | ||
o.ProjectIDs = []int64{b.ProjectID} | ||
o.SortType = "project-column-sorting" | ||
})) | ||
if err != nil { | ||
|
@@ -96,71 +90,88 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is | |
|
||
// IssueAssignOrRemoveProject changes the project associated with an issue | ||
// If newProjectID is 0, the issue is removed from the project | ||
func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error { | ||
func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectIDs []int64, newColumnID int64) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How could it be right? Why |
||
return db.WithTx(ctx, func(ctx context.Context) error { | ||
oldProjectID := issue.projectID(ctx) | ||
oldProjectIDs := issue.projectIDs(ctx) | ||
|
||
if err := issue.LoadRepo(ctx); err != nil { | ||
return err | ||
} | ||
|
||
// Only check if we add a new project and not remove it. | ||
if newProjectID > 0 { | ||
newProject, err := project_model.GetProjectByID(ctx, newProjectID) | ||
if err != nil { | ||
projectDB := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID) | ||
newProjectIDs, oldProjectIDs := util.DiffSlice(oldProjectIDs, newProjectIDs) | ||
|
||
if len(oldProjectIDs) > 0 { | ||
if _, err := projectDB.Where("issue_id=?", issue.ID).In("project_id", oldProjectIDs).Delete(&project_model.ProjectIssue{}); err != nil { | ||
return err | ||
} | ||
if !newProject.CanBeAccessedByOwnerRepo(issue.Repo.OwnerID, issue.Repo) { | ||
return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID) | ||
} | ||
if newColumnID == 0 { | ||
newDefaultColumn, err := newProject.MustDefaultColumn(ctx) | ||
if err != nil { | ||
for _, pID := range oldProjectIDs { | ||
if _, err := CreateComment(ctx, &CreateCommentOptions{ | ||
Type: CommentTypeProject, | ||
Doer: doer, | ||
Repo: issue.Repo, | ||
Issue: issue, | ||
OldProjectID: pID, | ||
ProjectID: 0, | ||
}); err != nil { | ||
return err | ||
} | ||
newColumnID = newDefaultColumn.ID | ||
} | ||
} | ||
|
||
if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { | ||
if len(newProjectIDs) == 0 { | ||
return nil | ||
} | ||
|
||
res := struct { | ||
MaxSorting int64 | ||
IssueCount int64 | ||
}{} | ||
if _, err := projectDB.Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue"). | ||
In("project_id", newProjectIDs). | ||
And("project_board_id=?", newColumnID). | ||
Get(&res); err != nil { | ||
return err | ||
} | ||
newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) | ||
|
||
pi := make([]*project_model.ProjectIssue, 0, len(newProjectIDs)) | ||
|
||
for _, pID := range newProjectIDs { | ||
if pID == 0 { | ||
continue | ||
} | ||
newProject, err := project_model.GetProjectByID(ctx, pID) | ||
if err != nil { | ||
return err | ||
} | ||
if !newProject.CanBeAccessedByOwnerRepo(issue.Repo.OwnerID, issue.Repo) { | ||
return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID) | ||
} | ||
|
||
pi = append(pi, &project_model.ProjectIssue{ | ||
IssueID: issue.ID, | ||
ProjectID: pID, | ||
ProjectColumnID: newColumnID, | ||
Sorting: newSorting, | ||
}) | ||
|
||
if oldProjectID > 0 || newProjectID > 0 { | ||
if _, err := CreateComment(ctx, &CreateCommentOptions{ | ||
Type: CommentTypeProject, | ||
Doer: doer, | ||
Repo: issue.Repo, | ||
Issue: issue, | ||
OldProjectID: oldProjectID, | ||
ProjectID: newProjectID, | ||
OldProjectID: 0, | ||
ProjectID: pID, | ||
}); err != nil { | ||
return err | ||
} | ||
} | ||
if newProjectID == 0 { | ||
return nil | ||
} | ||
if newColumnID == 0 { | ||
panic("newColumnID must not be zero") // shouldn't happen | ||
} | ||
|
||
res := struct { | ||
MaxSorting int64 | ||
IssueCount int64 | ||
}{} | ||
if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue"). | ||
Where("project_id=?", newProjectID). | ||
And("project_board_id=?", newColumnID). | ||
Get(&res); err != nil { | ||
return err | ||
if len(pi) > 0 { | ||
return db.Insert(ctx, pi) | ||
} | ||
newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) | ||
return db.Insert(ctx, &project_model.ProjectIssue{ | ||
IssueID: issue.ID, | ||
ProjectID: newProjectID, | ||
ProjectColumnID: newColumnID, | ||
Sorting: newSorting, | ||
}) | ||
|
||
return nil | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,7 +30,7 @@ type IndexerData struct { | |
LabelIDs []int64 `json:"label_ids"` | ||
NoLabel bool `json:"no_label"` // True if LabelIDs is empty | ||
MilestoneID int64 `json:"milestone_id"` | ||
ProjectID int64 `json:"project_id"` | ||
ProjectIDs []int64 `json:"project_id"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not think the change is right for indexers, I am pretty sure it breaks existing indexers and the JSON field name doesn't match. |
||
ProjectColumnID int64 `json:"project_board_id"` // the key should be kept as project_board_id to keep compatible | ||
PosterID int64 `json:"poster_id"` | ||
AssigneeID int64 `json:"assignee_id"` | ||
|
@@ -94,7 +94,7 @@ type SearchOptions struct { | |
|
||
MilestoneIDs []int64 // milestones the issues have | ||
|
||
ProjectID optional.Option[int64] // project the issues belong to | ||
ProjectIDs []int64 // project the issues belong to | ||
ProjectColumnID optional.Option[int64] // project column the issues belong to | ||
|
||
PosterID string // poster of the issues, "(none)" or "(any)" or a user ID | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It causes duplicate "loads".
If should check
issue.Projects == nil
, and below it should setissue.Projects
to an empty slice but not nil if there is no project.