-
-
Notifications
You must be signed in to change notification settings - Fork 6.4k
Allow multiple projects per issue and pull requests #36784
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
Open
icyavocado
wants to merge
71
commits into
go-gitea:main
Choose a base branch
from
icyavocado:issue-12974-multiple-projects-per-issue
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+664
−206
Open
Changes from all commits
Commits
Show all changes
71 commits
Select commit
Hold shift + click to select a range
18aa6fe
Added multi-project feature
tyroneyeh b64fcea
Fix board column move issue
tyroneyeh abfac6f
Try fixing your unit tests
tyroneyeh 8f2483d
Fix lint issue
tyroneyeh 020aa0d
Fix add issue to multiple project issue
tyroneyeh db2704f
Remove lessuse code
tyroneyeh 8352a06
Fix search empty issue
tyroneyeh 9363b0c
Improject filter search method
tyroneyeh 8758420
Try fix unit test fail
tyroneyeh 3fe8491
Try fix unit test issue
tyroneyeh d7569fd
Fix no project filter list issue
tyroneyeh 56e3dcd
Fix unit test on TestIssueList_LoadAttributes
tyroneyeh 62b5db4
Fix unit test on Test_Projects fail
tyroneyeh 0065dfa
Fix unit test on TestIssueLoadAttributes
tyroneyeh 53afa55
Adjuect from Cols to Select
tyroneyeh aec0e54
Improve adjust A to B project bug
tyroneyeh a4265d7
Improve string join code for projects id
tyroneyeh 9365b45
Fix on issue list clear project issue
tyroneyeh ff29541
Change db.Exec to Update method
tyroneyeh a4844eb
Rename LoadProject to LoadProjects
tyroneyeh a527007
Rename variable projectIDs to ids
tyroneyeh 1f23c36
Revert "Rename variable projectIDs to ids"
tyroneyeh 6d53378
Fix creating new issue redirect link from project
tyroneyeh 1e84279
Fix LoadProjects function
tyroneyeh 226c0fb
Modify indexer json field name
tyroneyeh d0bd2de
Fix unit test
tyroneyeh fed56be
Fix lint check error
tyroneyeh 7e46251
Fix unit test
tyroneyeh 4c4bb03
[FIX] check to make sure SQL returns result, and without error
icyavocado 3416b08
[FIX] Refactor projectIDs method to handle project retrieval and erro…
icyavocado aec3e71
[FIX] Rename variable for clarity in IssueAssignOrRemoveProject function
icyavocado c8723c6
[FIX] Update project list styling to use 'relaxed' class for improved…
icyavocado 95677c2
[FIX] Handle error in LoadProjects when retrieving project IDs
icyavocado 0338162
[FIX] Update projectIDs function to return error on database query fa…
icyavocado c61f6e3
[FIX] Log warnings for issues that fail to assign projects due to per…
icyavocado 58dfdbc
[FIX] Enhance LoadProjects to handle nil and empty project list cases
icyavocado a709e0b
[FIX] Handle error when retrieving old project IDs in IssueAssignOrRe…
icyavocado d047cdb
Add comprehensive test suite for multiple projects per issue function…
icyavocado 2aad2b5
[FIX] Update project_id to project_ids across indexers for consistency
Copilot 6d698e8
[FIX] Update handling of ProjectIDs in ToDBOptions and related tests
Copilot 21316ab
[FIX] Update project ID handling in search queries and increment inde…
icyavocado b03696c
[FIX] Rename project_id to project_ids for consistency in IndexerData…
icyavocado 69d3f72
[FIX] Simplify ProjectIDs check using slices.Contains in indexer tests
icyavocado 8199f85
[FIX] Replace RemoveValue with SliceRemoveAll for better project ID h…
icyavocado 01b1ff8
[FIX] Refactor project ID handling to use strings.Join for improved c…
icyavocado 3d6ceb0
[FIX] find default column instead of default to uncategorized
icyavocado a834c35
[FIX] Refactor projectID handling to use explicit projectIDs slice li…
Copilot 96bc253
[FIX] add isProjectsLoaded flag following milestone pattern
Copilot 8f22b88
[FIX] update queries in IssueAssignOrRemoveProject
Copilot f0f9125
[FIX] using a new isuesProject map to access Projects faster
Copilot 0bef347
[FIX] update documentation from LoadProject to LoadProjects
icyavocado f8db5c6
[DOCUMENTATION] update documentation to show mulitple projectIds can …
icyavocado 97b4d81
[FIX] remove projectID passing in incorrectly
icyavocado 6217d8d
[DOCUMENTATION] update opts.ProjectID to opts.ProjectIDs
icyavocado 31a7ca1
[FIX] rename variables to projectsToAdd and projectsToRemove
Copilot 1317d3c
[FIX] make DiffSlice preserve input slice order
Copilot 1a65ba1
[FIX] update ProjectIDs declaration
icyavocado a0cbb87
[FIX] handle projectId NoConditionID
icyavocado ecf4dcd
[FIX] convert projectID from optional.Option[int64] to []int64 for Pr…
Copilot d01eef2
[FIX] update to ProjectIDs
icyavocado 6b01e33
[FIX] use projectIDs variable for IndexerData.ProjectIDs in test data…
Copilot 1919cff
[FIX] compute issue-project sorting per (project_id, project_board_id)
Copilot e0e49de
[FIX] avoid mutating opts.ProjectIDs in applyProjectCondition
Copilot 3845959
[FIX] streamline projectID handling in SearchRepoIssuesJSON
icyavocado 8fe37bf
[FIX] update projectID handling in SearchIssues to support multiple p…
icyavocado 6f9bb32
[FIX] format SearchRepoIssuesJSON parameters for consistency
icyavocado a5dbb63
[E2E] add e2e test for projects
icyavocado a5a2af2
[FIX] update class name from milestone list to project-list
icyavocado b4b07b9
[FIX] update class name from milestone list to project-list in e2e test
icyavocado eb15fb7
[FIX] reset isProjectsLoaded variable before re-LoadProjects
icyavocado 0edf345
[E2E] remove e2e tests since it will take about 14s to finish
icyavocado File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,29 +12,34 @@ import ( | |
| "code.gitea.io/gitea/modules/util" | ||
| ) | ||
|
|
||
| // 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"). | ||
| // LoadProjects loads all projects the issue is assigned to | ||
| func (issue *Issue) LoadProjects(ctx context.Context) (err error) { | ||
| if !issue.isProjectsLoaded { | ||
| 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) | ||
| if err == nil { | ||
| issue.isProjectsLoaded = true | ||
| } | ||
| } | ||
| 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, error) { | ||
| var pis []project_model.ProjectIssue | ||
| if err := db.GetEngine(ctx).Table("project_issue").Where("issue_id = ?", issue.ID).Cols("project_id").Find(&pis); err != nil { | ||
| return nil, err | ||
| } | ||
| return ip.ProjectID | ||
|
|
||
| if len(pis) == 0 { | ||
| return []int64{}, nil | ||
| } | ||
|
|
||
| ids := make([]int64, 0, len(pis)) | ||
| for _, pi := range pis { | ||
| ids = append(ids, pi.ProjectID) | ||
| } | ||
| return ids, nil | ||
| } | ||
|
|
||
| // ProjectColumnID return project column id if issue was assigned to one | ||
|
|
@@ -68,7 +73,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 +83,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 { | ||
|
|
@@ -94,73 +99,103 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is | |
| return issueList, nil | ||
| } | ||
|
|
||
| // 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 { | ||
| // IssueAssignOrRemoveProject updates the projects associated with an issue. | ||
| // It adds projects that are in newProjectIDs but not currently assigned, and removes | ||
| // projects that are currently assigned but not in newProjectIDs. If newProjectIDs is | ||
| // empty or nil, all projects are removed from the issue. | ||
| func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectIDs []int64, newColumnID int64) error { | ||
|
Author
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.
Target the default column of a project (if exists) |
||
| return db.WithTx(ctx, func(ctx context.Context) error { | ||
| oldProjectID := issue.projectID(ctx) | ||
| oldProjectIDs, err := issue.projectIDs(ctx) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| 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) | ||
| projectsToAdd, projectsToRemove := util.DiffSlice(oldProjectIDs, newProjectIDs) | ||
|
|
||
| if len(projectsToRemove) > 0 { | ||
| if _, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).In("project_id", projectsToRemove).Delete(&project_model.ProjectIssue{}); err != nil { | ||
| return err | ||
| } | ||
| for _, projectID := range projectsToRemove { | ||
| if _, err := CreateComment(ctx, &CreateCommentOptions{ | ||
| Type: CommentTypeProject, | ||
| Doer: doer, | ||
| Repo: issue.Repo, | ||
| Issue: issue, | ||
| OldProjectID: projectID, | ||
| ProjectID: 0, | ||
| }); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if len(projectsToAdd) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| pi := make([]*project_model.ProjectIssue, 0, len(projectsToAdd)) | ||
|
|
||
| for _, projectID := range projectsToAdd { | ||
| if projectID == 0 { | ||
| continue | ||
| } | ||
| newProject, err := project_model.GetProjectByID(ctx, projectID) | ||
| 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) | ||
| } | ||
| if newColumnID == 0 { | ||
| newDefaultColumn, err := newProject.MustDefaultColumn(ctx) | ||
|
|
||
| projectColumnID := newColumnID | ||
| if projectColumnID == 0 { | ||
| defaultColumn, err := newProject.MustDefaultColumn(ctx) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| newColumnID = newDefaultColumn.ID | ||
| projectColumnID = defaultColumn.ID | ||
| } | ||
| } | ||
|
|
||
| if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil { | ||
| return err | ||
| } | ||
| res := struct { | ||
| MaxSorting int64 | ||
| IssueCount int64 | ||
| }{} | ||
| if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as issue_count").Table("project_issue"). | ||
| And("project_id=?", projectID). | ||
| And("project_board_id=?", projectColumnID). | ||
| Get(&res); err != nil { | ||
| return err | ||
| } | ||
| newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0) | ||
|
|
||
| pi = append(pi, &project_model.ProjectIssue{ | ||
| IssueID: issue.ID, | ||
| ProjectID: projectID, | ||
| ProjectColumnID: projectColumnID, | ||
| 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: projectID, | ||
| }); 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 | ||
| }) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
#34386 (comment)
Catched error and return nill.