Skip to content

Commit d48061b

Browse files
committed
Added multi-project feature
1 parent 6bd8fe5 commit d48061b

File tree

19 files changed

+185
-131
lines changed

19 files changed

+185
-131
lines changed

models/issues/issue.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,17 @@ type Issue struct {
7474
PosterID int64 `xorm:"INDEX"`
7575
Poster *user_model.User `xorm:"-"`
7676
OriginalAuthor string
77-
OriginalAuthorID int64 `xorm:"index"`
78-
Title string `xorm:"name"`
79-
Content string `xorm:"LONGTEXT"`
80-
RenderedContent template.HTML `xorm:"-"`
81-
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
82-
Labels []*Label `xorm:"-"`
83-
isLabelsLoaded bool `xorm:"-"`
84-
MilestoneID int64 `xorm:"INDEX"`
85-
Milestone *Milestone `xorm:"-"`
86-
isMilestoneLoaded bool `xorm:"-"`
87-
Project *project_model.Project `xorm:"-"`
77+
OriginalAuthorID int64 `xorm:"index"`
78+
Title string `xorm:"name"`
79+
Content string `xorm:"LONGTEXT"`
80+
RenderedContent template.HTML `xorm:"-"`
81+
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
82+
Labels []*Label `xorm:"-"`
83+
isLabelsLoaded bool `xorm:"-"`
84+
MilestoneID int64 `xorm:"INDEX"`
85+
Milestone *Milestone `xorm:"-"`
86+
isMilestoneLoaded bool `xorm:"-"`
87+
Projects []*project_model.Project `xorm:"-"`
8888
Priority int
8989
AssigneeID int64 `xorm:"-"`
9090
Assignee *user_model.User `xorm:"-"`

models/issues/issue_list.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,14 +219,19 @@ func (issues IssueList) LoadProjects(ctx context.Context) error {
219219
return err
220220
}
221221
for _, project := range projects {
222-
projectMaps[project.IssueID] = project.Project
222+
projectMaps[project.ID] = project.Project
223223
}
224224
left -= limit
225225
issueIDs = issueIDs[limit:]
226226
}
227227

228228
for _, issue := range issues {
229-
issue.Project = projectMaps[issue.ID]
229+
projectIDs := issue.projectIDs(ctx)
230+
for _, i := range projectIDs {
231+
if projectMaps[i] != nil {
232+
issue.Projects = append(issue.Projects, projectMaps[i])
233+
}
234+
}
230235
}
231236
return nil
232237
}

models/issues/issue_list_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@ func TestIssueList_LoadAttributes(t *testing.T) {
6666
}
6767
if issue.ID == int64(1) {
6868
assert.Equal(t, int64(400), issue.TotalTrackedTime)
69-
assert.NotNil(t, issue.Project)
70-
assert.Equal(t, int64(1), issue.Project.ID)
69+
assert.NotNil(t, issue.Projects[0])
70+
assert.Equal(t, int64(1), issue.Projects[0].ID)
7171
} else {
72-
assert.Nil(t, issue.Project)
72+
assert.Nil(t, issue.Projects[0])
7373
}
7474
}
7575
}

models/issues/issue_project.go

Lines changed: 64 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,21 @@ import (
1414

1515
// LoadProject load the project the issue was assigned to
1616
func (issue *Issue) LoadProject(ctx context.Context) (err error) {
17-
if issue.Project == nil {
18-
var p project_model.Project
19-
has, err := db.GetEngine(ctx).Table("project").
17+
if len(issue.Projects) == 0 {
18+
err = db.GetEngine(ctx).Table("project").
2019
Join("INNER", "project_issue", "project.id=project_issue.project_id").
21-
Where("project_issue.issue_id = ?", issue.ID).Get(&p)
22-
if err != nil {
23-
return err
24-
} else if has {
25-
issue.Project = &p
26-
}
20+
Where("project_issue.issue_id = ?", issue.ID).Find(&issue.Projects)
2721
}
2822
return err
2923
}
3024

31-
func (issue *Issue) projectID(ctx context.Context) int64 {
32-
var ip project_model.ProjectIssue
33-
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
34-
if err != nil || !has {
35-
return 0
25+
func (issue *Issue) projectIDs(ctx context.Context) []int64 {
26+
var ids []int64
27+
if err := db.GetEngine(ctx).Table("project_issue").Where("issue_id=?", issue.ID).Cols("project_id").Find(&ids); err != nil {
28+
return nil
3629
}
37-
return ip.ProjectID
30+
31+
return ids
3832
}
3933

4034
// ProjectColumnID return project column id if issue was assigned to one
@@ -96,17 +90,52 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is
9690

9791
// IssueAssignOrRemoveProject changes the project associated with an issue
9892
// If newProjectID is 0, the issue is removed from the project
99-
func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error {
93+
func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectIDs []int64, newColumnID int64) error {
10094
return db.WithTx(ctx, func(ctx context.Context) error {
101-
oldProjectID := issue.projectID(ctx)
95+
oldProjectIDs := issue.projectIDs(ctx)
10296

10397
if err := issue.LoadRepo(ctx); err != nil {
10498
return err
10599
}
106100

107-
// Only check if we add a new project and not remove it.
108-
if newProjectID > 0 {
109-
newProject, err := project_model.GetProjectByID(ctx, newProjectID)
101+
projectDB := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID)
102+
newProjectIDs, oldProjectIDs := util.DiffSlice(oldProjectIDs, newProjectIDs)
103+
104+
if len(oldProjectIDs) > 0 {
105+
if _, err := projectDB.Where("issue_id=?", issue.ID).In("project_id", oldProjectIDs).Delete(&project_model.ProjectIssue{}); err != nil {
106+
return err
107+
}
108+
for _, pID := range oldProjectIDs {
109+
if _, err := CreateComment(ctx, &CreateCommentOptions{
110+
Type: CommentTypeProject,
111+
Doer: doer,
112+
Repo: issue.Repo,
113+
Issue: issue,
114+
OldProjectID: pID,
115+
ProjectID: 0,
116+
}); err != nil {
117+
return err
118+
}
119+
}
120+
return nil
121+
}
122+
123+
res := struct {
124+
MaxSorting int64
125+
IssueCount int64
126+
}{}
127+
if _, err := projectDB.Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue").
128+
In("project_id", newProjectIDs).
129+
And("project_board_id=?", newColumnID).
130+
Get(&res); err != nil {
131+
return err
132+
}
133+
newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
134+
135+
pi := make([]*project_model.ProjectIssue, 0, len(newProjectIDs))
136+
137+
for _, pID := range newProjectIDs {
138+
newProject, err := project_model.GetProjectByID(ctx, pID)
110139
if err != nil {
111140
return err
112141
}
@@ -119,48 +148,34 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
119148
return err
120149
}
121150
newColumnID = newDefaultColumn.ID
151+
if newColumnID == 0 {
152+
panic("newColumnID must not be zero") // shouldn't happen
153+
}
122154
}
123-
}
124155

125-
if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil {
126-
return err
127-
}
156+
pi = append(pi, &project_model.ProjectIssue{
157+
IssueID: issue.ID,
158+
ProjectID: pID,
159+
ProjectColumnID: newColumnID,
160+
Sorting: newSorting,
161+
})
128162

129-
if oldProjectID > 0 || newProjectID > 0 {
130163
if _, err := CreateComment(ctx, &CreateCommentOptions{
131164
Type: CommentTypeProject,
132165
Doer: doer,
133166
Repo: issue.Repo,
134167
Issue: issue,
135-
OldProjectID: oldProjectID,
136-
ProjectID: newProjectID,
168+
OldProjectID: 0,
169+
ProjectID: pID,
137170
}); err != nil {
138171
return err
139172
}
140173
}
141-
if newProjectID == 0 {
142-
return nil
143-
}
144-
if newColumnID == 0 {
145-
panic("newColumnID must not be zero") // shouldn't happen
146-
}
147174

148-
res := struct {
149-
MaxSorting int64
150-
IssueCount int64
151-
}{}
152-
if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue").
153-
Where("project_id=?", newProjectID).
154-
And("project_board_id=?", newColumnID).
155-
Get(&res); err != nil {
156-
return err
175+
if len(pi) > 0 {
176+
return db.Insert(ctx, pi)
157177
}
158-
newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
159-
return db.Insert(ctx, &project_model.ProjectIssue{
160-
IssueID: issue.ID,
161-
ProjectID: newProjectID,
162-
ProjectColumnID: newColumnID,
163-
Sorting: newSorting,
164-
})
178+
179+
return nil
165180
})
166181
}

models/issues/issue_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -417,10 +417,10 @@ func TestIssueLoadAttributes(t *testing.T) {
417417
}
418418
if issue.ID == int64(1) {
419419
assert.Equal(t, int64(400), issue.TotalTrackedTime)
420-
assert.NotNil(t, issue.Project)
421-
assert.Equal(t, int64(1), issue.Project.ID)
420+
assert.NotNil(t, issue.Projects[0])
421+
assert.Equal(t, int64(1), issue.Projects[0].ID)
422422
} else {
423-
assert.Nil(t, issue.Project)
423+
assert.Nil(t, issue.Projects[0])
424424
}
425425
}
426426
}

modules/indexer/issues/internal/model.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type IndexerData struct {
3030
LabelIDs []int64 `json:"label_ids"`
3131
NoLabel bool `json:"no_label"` // True if LabelIDs is empty
3232
MilestoneID int64 `json:"milestone_id"`
33-
ProjectID int64 `json:"project_id"`
33+
ProjectIDs []int64 `json:"project_id"`
3434
ProjectColumnID int64 `json:"project_board_id"` // the key should be kept as project_board_id to keep compatible
3535
PosterID int64 `json:"poster_id"`
3636
AssigneeID int64 `json:"assignee_id"`

modules/indexer/issues/internal/tests/tests.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ var cases = []*testIndexerCase{
302302
},
303303
},
304304
{
305-
Name: "ProjectID",
305+
Name: "ProjectIDs",
306306
SearchOptions: &internal.SearchOptions{
307307
Paginator: &db.ListOptions{
308308
PageSize: 5,
@@ -312,10 +312,10 @@ var cases = []*testIndexerCase{
312312
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
313313
assert.Len(t, result.Hits, 5)
314314
for _, v := range result.Hits {
315-
assert.Equal(t, int64(1), data[v.ID].ProjectID)
315+
assert.Equal(t, int64(1), data[v.ID].ProjectIDs[0])
316316
}
317317
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
318-
return v.ProjectID == 1
318+
return v.ProjectIDs[0] == 1
319319
}), result.Total)
320320
},
321321
},
@@ -330,10 +330,10 @@ var cases = []*testIndexerCase{
330330
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
331331
assert.Len(t, result.Hits, 5)
332332
for _, v := range result.Hits {
333-
assert.Equal(t, int64(0), data[v.ID].ProjectID)
333+
assert.Equal(t, int64(0), data[v.ID].ProjectIDs[0])
334334
}
335335
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
336-
return v.ProjectID == 0
336+
return v.ProjectIDs[0] == 0
337337
}), result.Total)
338338
},
339339
},
@@ -691,6 +691,10 @@ func generateDefaultIndexerData() []*internal.IndexerData {
691691
for i := range labelIDs {
692692
labelIDs[i] = int64(i) + 1 // LabelID should not be 0
693693
}
694+
projectIDs := make([]int64, id%5)
695+
for i := range projectIDs {
696+
projectIDs[i] = int64(i) + 1 // projectIDs should not be 0
697+
}
694698
mentionIDs := make([]int64, id%6)
695699
for i := range mentionIDs {
696700
mentionIDs[i] = int64(i) + 1 // MentionID should not be 0
@@ -720,7 +724,7 @@ func generateDefaultIndexerData() []*internal.IndexerData {
720724
LabelIDs: labelIDs,
721725
NoLabel: len(labelIDs) == 0,
722726
MilestoneID: issueIndex % 4,
723-
ProjectID: issueIndex % 5,
727+
ProjectIDs: projectIDs,
724728
ProjectColumnID: issueIndex % 6,
725729
PosterID: id%10 + 1, // PosterID should not be 0
726730
AssigneeID: issueIndex % 10,

modules/indexer/issues/util.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
8787
return nil, false, err
8888
}
8989

90-
var projectID int64
91-
if issue.Project != nil {
92-
projectID = issue.Project.ID
90+
projectIDs := make([]int64, 0, len(issue.Projects))
91+
for _, project := range issue.Projects {
92+
projectIDs = append(projectIDs, project.ID)
9393
}
9494

9595
projectColumnID, err := issue.ProjectColumnID(ctx)
@@ -110,7 +110,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
110110
LabelIDs: labels,
111111
NoLabel: len(labels) == 0,
112112
MilestoneID: issue.MilestoneID,
113-
ProjectID: projectID,
113+
ProjectIDs: projectIDs,
114114
ProjectColumnID: projectColumnID,
115115
PosterID: issue.PosterID,
116116
AssigneeID: issue.AssigneeID,

modules/util/util.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,27 @@ func ReserveLineBreakForTextarea(input string) string {
253253
// Other than this, we should respect the original content, even leading or trailing spaces.
254254
return strings.ReplaceAll(input, "\r\n", "\n")
255255
}
256+
257+
func DiffSlice[T comparable](oldSlice, newSlice []T) (added, removed []T) {
258+
oldSet := make(map[T]struct{}, len(oldSlice))
259+
newSet := make(map[T]struct{}, len(newSlice))
260+
261+
for _, v := range oldSlice {
262+
oldSet[v] = struct{}{}
263+
}
264+
for _, v := range newSlice {
265+
newSet[v] = struct{}{}
266+
}
267+
268+
for v := range newSet {
269+
if _, found := oldSet[v]; !found {
270+
added = append(added, v)
271+
}
272+
}
273+
for v := range oldSet {
274+
if _, found := newSet[v]; !found {
275+
removed = append(removed, v)
276+
}
277+
}
278+
return added, removed
279+
}

routers/api/v1/repo/issue.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,7 @@ func CreateIssue(ctx *context.APIContext) {
721721
form.Labels = make([]int64, 0)
722722
}
723723

724-
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0); err != nil {
724+
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, nil); err != nil {
725725
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
726726
ctx.APIError(http.StatusBadRequest, err)
727727
} else if errors.Is(err, user_model.ErrBlockedUser) {

0 commit comments

Comments
 (0)