mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
152 lines
4.6 KiB
Go
152 lines
4.6 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package issues
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/hanzoai/git/models/db"
|
|
project_model "github.com/hanzoai/git/models/project"
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
"github.com/hanzoai/git/modules/util"
|
|
)
|
|
|
|
// 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).
|
|
OrderBy("project.id ASC").
|
|
Find(&issue.Projects)
|
|
if err == nil {
|
|
issue.isProjectsLoaded = true
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (issue *Issue) projectIDs(ctx context.Context) (projectIDs []int64, _ error) {
|
|
err := db.GetEngine(ctx).Table("project_issue").Where("issue_id = ?", issue.ID).Cols("project_id").Find(&projectIDs)
|
|
return projectIDs, err
|
|
}
|
|
|
|
// ProjectColumnMap returns a map of project ID to column ID for this issue.
|
|
func (issue *Issue) ProjectColumnMap(ctx context.Context) (map[int64]int64, error) {
|
|
var projIssues []project_model.ProjectIssue
|
|
if err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Find(&projIssues); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make(map[int64]int64, len(projIssues))
|
|
for _, projIssue := range projIssues {
|
|
result[projIssue.ProjectID] = projIssue.ProjectColumnID
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func LoadProjectIssueColumnMap(ctx context.Context, projectID, defaultColumnID int64) (map[int64]int64, error) {
|
|
issues := make([]project_model.ProjectIssue, 0)
|
|
if err := db.GetEngine(ctx).Where("project_id=?", projectID).Find(&issues); err != nil {
|
|
return nil, err
|
|
}
|
|
result := make(map[int64]int64, len(issues))
|
|
for _, issue := range issues {
|
|
if issue.ProjectColumnID == 0 {
|
|
issue.ProjectColumnID = defaultColumnID
|
|
}
|
|
result[issue.IssueID] = issue.ProjectColumnID
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// 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, all projects are removed from the issue.
|
|
// When adding an issue to a project, it is placed in the project's default column.
|
|
func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectIDs []int64) error {
|
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
if err := issue.LoadRepo(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
oldProjectIDs, err := issue.projectIDs(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
projectsToAdd, projectsToRemove := util.DiffSlice(oldProjectIDs, newProjectIDs)
|
|
issue.isProjectsLoaded = false
|
|
issue.Projects = nil
|
|
|
|
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 {
|
|
projectMap, err := project_model.GetProjectsMapByIDs(ctx, projectsToAdd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, projectID := range projectsToAdd {
|
|
newProject, ok := projectMap[projectID]
|
|
if !ok {
|
|
return util.NewNotExistErrorf("project %d not found", projectID)
|
|
}
|
|
if !newProject.CanBeAccessedByOwnerRepo(issue.Repo.OwnerID, issue.Repo) {
|
|
return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID)
|
|
}
|
|
|
|
defaultColumn, err := newProject.MustDefaultColumn(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newSorting, err := project_model.GetColumnIssueNextSorting(ctx, projectID, defaultColumn.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = db.Insert(ctx, &project_model.ProjectIssue{
|
|
IssueID: issue.ID,
|
|
ProjectID: projectID,
|
|
ProjectColumnID: defaultColumn.ID,
|
|
Sorting: newSorting,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := CreateComment(ctx, &CreateCommentOptions{
|
|
Type: CommentTypeProject,
|
|
Doer: doer,
|
|
Repo: issue.Repo,
|
|
Issue: issue,
|
|
OldProjectID: 0,
|
|
ProjectID: projectID,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|