mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-29 03:38:52 +03:00
Merge pull request '[gitea] week 2024-23 cherry pick (gitea/main -> forgejo)' (#3989) from earl-warren/wcp/2024-23 into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3989 Reviewed-by: Otto <otto@codeberg.org>
This commit is contained in:
commit
c2382d4f5b
98 changed files with 1520 additions and 975 deletions
|
@ -77,7 +77,7 @@ package "code.gitea.io/gitea/models/perm/access"
|
||||||
func GetRepoWriters
|
func GetRepoWriters
|
||||||
|
|
||||||
package "code.gitea.io/gitea/models/project"
|
package "code.gitea.io/gitea/models/project"
|
||||||
func UpdateBoardSorting
|
func UpdateColumnSorting
|
||||||
func ChangeProjectStatus
|
func ChangeProjectStatus
|
||||||
|
|
||||||
package "code.gitea.io/gitea/models/repo"
|
package "code.gitea.io/gitea/models/repo"
|
||||||
|
|
|
@ -1924,7 +1924,10 @@ LEVEL = Info
|
||||||
;; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
|
;; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
|
||||||
;MINIO_ENDPOINT = localhost:9000
|
;MINIO_ENDPOINT = localhost:9000
|
||||||
;;
|
;;
|
||||||
;; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
|
;; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`.
|
||||||
|
;; If not provided and STORAGE_TYPE is `minio`, will search for credentials in known
|
||||||
|
;; environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files
|
||||||
|
;; (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
|
||||||
;MINIO_ACCESS_KEY_ID =
|
;MINIO_ACCESS_KEY_ID =
|
||||||
;;
|
;;
|
||||||
;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
|
;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
|
||||||
|
@ -2633,7 +2636,10 @@ LEVEL = Info
|
||||||
;; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
|
;; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
|
||||||
;MINIO_ENDPOINT = localhost:9000
|
;MINIO_ENDPOINT = localhost:9000
|
||||||
;;
|
;;
|
||||||
;; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
|
;; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`.
|
||||||
|
;; If not provided and STORAGE_TYPE is `minio`, will search for credentials in known
|
||||||
|
;; environment variables (MINIO_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID), credentials files
|
||||||
|
;; (~/.mc/config.json, ~/.aws/credentials), and EC2 instance metadata.
|
||||||
;MINIO_ACCESS_KEY_ID =
|
;MINIO_ACCESS_KEY_ID =
|
||||||
;;
|
;;
|
||||||
;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
|
;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
|
||||||
|
|
|
@ -54,7 +54,6 @@ type FindTaskOptions struct {
|
||||||
UpdatedBefore timeutil.TimeStamp
|
UpdatedBefore timeutil.TimeStamp
|
||||||
StartedBefore timeutil.TimeStamp
|
StartedBefore timeutil.TimeStamp
|
||||||
RunnerID int64
|
RunnerID int64
|
||||||
IDOrderDesc bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts FindTaskOptions) ToConds() builder.Cond {
|
func (opts FindTaskOptions) ToConds() builder.Cond {
|
||||||
|
@ -84,8 +83,5 @@ func (opts FindTaskOptions) ToConds() builder.Cond {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts FindTaskOptions) ToOrders() string {
|
func (opts FindTaskOptions) ToOrders() string {
|
||||||
if opts.IDOrderDesc {
|
return "`id` DESC"
|
||||||
return "`id` DESC"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ type Statistic struct {
|
||||||
Mirror, Release, AuthSource, Webhook,
|
Mirror, Release, AuthSource, Webhook,
|
||||||
Milestone, Label, HookTask,
|
Milestone, Label, HookTask,
|
||||||
Team, UpdateTask, Project,
|
Team, UpdateTask, Project,
|
||||||
ProjectBoard, Attachment,
|
ProjectColumn, Attachment,
|
||||||
Branches, Tags, CommitStatus int64
|
Branches, Tags, CommitStatus int64
|
||||||
IssueByLabel []IssueByLabelCount
|
IssueByLabel []IssueByLabelCount
|
||||||
IssueByRepository []IssueByRepositoryCount
|
IssueByRepository []IssueByRepositoryCount
|
||||||
|
@ -115,6 +115,6 @@ func GetStatistic(ctx context.Context) (stats Statistic) {
|
||||||
stats.Counter.Team, _ = e.Count(new(organization.Team))
|
stats.Counter.Team, _ = e.Count(new(organization.Team))
|
||||||
stats.Counter.Attachment, _ = e.Count(new(repo_model.Attachment))
|
stats.Counter.Attachment, _ = e.Count(new(repo_model.Attachment))
|
||||||
stats.Counter.Project, _ = e.Count(new(project_model.Project))
|
stats.Counter.Project, _ = e.Count(new(project_model.Project))
|
||||||
stats.Counter.ProjectBoard, _ = e.Count(new(project_model.Board))
|
stats.Counter.ProjectColumn, _ = e.Count(new(project_model.Column))
|
||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,17 +88,13 @@ func (opts FindBranchOptions) ToConds() builder.Cond {
|
||||||
|
|
||||||
func (opts FindBranchOptions) ToOrders() string {
|
func (opts FindBranchOptions) ToOrders() string {
|
||||||
orderBy := opts.OrderBy
|
orderBy := opts.OrderBy
|
||||||
if opts.IsDeletedBranch.ValueOrDefault(true) { // if deleted branch included, put them at the end
|
|
||||||
if orderBy != "" {
|
|
||||||
orderBy += ", "
|
|
||||||
}
|
|
||||||
orderBy += "is_deleted ASC"
|
|
||||||
}
|
|
||||||
if orderBy == "" {
|
if orderBy == "" {
|
||||||
// the commit_time might be the same, so add the "name" to make sure the order is stable
|
// the commit_time might be the same, so add the "name" to make sure the order is stable
|
||||||
return "commit_time DESC, name ASC"
|
orderBy = "commit_time DESC, name ASC"
|
||||||
|
}
|
||||||
|
if opts.IsDeletedBranch.ValueOrDefault(true) { // if deleted branch included, put them at the beginning
|
||||||
|
orderBy = "is_deleted ASC, " + orderBy
|
||||||
}
|
}
|
||||||
|
|
||||||
return orderBy
|
return orderBy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,23 +27,27 @@ func init() {
|
||||||
|
|
||||||
// LoadAssignees load assignees of this issue.
|
// LoadAssignees load assignees of this issue.
|
||||||
func (issue *Issue) LoadAssignees(ctx context.Context) (err error) {
|
func (issue *Issue) LoadAssignees(ctx context.Context) (err error) {
|
||||||
|
if issue.isAssigneeLoaded || len(issue.Assignees) > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Reset maybe preexisting assignees
|
// Reset maybe preexisting assignees
|
||||||
issue.Assignees = []*user_model.User{}
|
issue.Assignees = []*user_model.User{}
|
||||||
issue.Assignee = nil
|
issue.Assignee = nil
|
||||||
|
|
||||||
err = db.GetEngine(ctx).Table("`user`").
|
if err = db.GetEngine(ctx).Table("`user`").
|
||||||
Join("INNER", "issue_assignees", "assignee_id = `user`.id").
|
Join("INNER", "issue_assignees", "assignee_id = `user`.id").
|
||||||
Where("issue_assignees.issue_id = ?", issue.ID).
|
Where("issue_assignees.issue_id = ?", issue.ID).
|
||||||
Find(&issue.Assignees)
|
Find(&issue.Assignees); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
issue.isAssigneeLoaded = true
|
||||||
// Check if we have at least one assignee and if yes put it in as `Assignee`
|
// Check if we have at least one assignee and if yes put it in as `Assignee`
|
||||||
if len(issue.Assignees) > 0 {
|
if len(issue.Assignees) > 0 {
|
||||||
issue.Assignee = issue.Assignees[0]
|
issue.Assignee = issue.Assignees[0]
|
||||||
}
|
}
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAssigneeIDsByIssue returns the IDs of users assigned to an issue
|
// GetAssigneeIDsByIssue returns the IDs of users assigned to an issue
|
||||||
|
|
|
@ -52,6 +52,8 @@ func (err ErrCommentNotExist) Unwrap() error {
|
||||||
return util.ErrNotExist
|
return util.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ErrCommentAlreadyChanged = util.NewInvalidArgumentErrorf("the comment is already changed")
|
||||||
|
|
||||||
// CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
|
// CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
|
||||||
type CommentType int
|
type CommentType int
|
||||||
|
|
||||||
|
@ -100,8 +102,8 @@ const (
|
||||||
CommentTypeMergePull // 28 merge pull request
|
CommentTypeMergePull // 28 merge pull request
|
||||||
CommentTypePullRequestPush // 29 push to PR head branch
|
CommentTypePullRequestPush // 29 push to PR head branch
|
||||||
|
|
||||||
CommentTypeProject // 30 Project changed
|
CommentTypeProject // 30 Project changed
|
||||||
CommentTypeProjectBoard // 31 Project board changed
|
CommentTypeProjectColumn // 31 Project column changed
|
||||||
|
|
||||||
CommentTypeDismissReview // 32 Dismiss Review
|
CommentTypeDismissReview // 32 Dismiss Review
|
||||||
|
|
||||||
|
@ -146,7 +148,7 @@ var commentStrings = []string{
|
||||||
"merge_pull",
|
"merge_pull",
|
||||||
"pull_push",
|
"pull_push",
|
||||||
"project",
|
"project",
|
||||||
"project_board",
|
"project_board", // FIXME: the name should be project_column
|
||||||
"dismiss_review",
|
"dismiss_review",
|
||||||
"change_issue_ref",
|
"change_issue_ref",
|
||||||
"pull_scheduled_merge",
|
"pull_scheduled_merge",
|
||||||
|
@ -262,6 +264,7 @@ type Comment struct {
|
||||||
Line int64 // - previous line / + proposed line
|
Line int64 // - previous line / + proposed line
|
||||||
TreePath string
|
TreePath string
|
||||||
Content string `xorm:"LONGTEXT"`
|
Content string `xorm:"LONGTEXT"`
|
||||||
|
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
||||||
RenderedContent template.HTML `xorm:"-"`
|
RenderedContent template.HTML `xorm:"-"`
|
||||||
|
|
||||||
// Path represents the 4 lines of code cemented by this comment
|
// Path represents the 4 lines of code cemented by this comment
|
||||||
|
@ -1119,7 +1122,7 @@ func UpdateCommentInvalidate(ctx context.Context, c *Comment) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateComment updates information of comment.
|
// UpdateComment updates information of comment.
|
||||||
func UpdateComment(ctx context.Context, c *Comment, doer *user_model.User) error {
|
func UpdateComment(ctx context.Context, c *Comment, contentVersion int, doer *user_model.User) error {
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -1139,9 +1142,15 @@ func UpdateComment(ctx context.Context, c *Comment, doer *user_model.User) error
|
||||||
// see https://codeberg.org/forgejo/forgejo/pulls/764#issuecomment-1023801
|
// see https://codeberg.org/forgejo/forgejo/pulls/764#issuecomment-1023801
|
||||||
c.UpdatedUnix = c.Issue.UpdatedUnix
|
c.UpdatedUnix = c.Issue.UpdatedUnix
|
||||||
}
|
}
|
||||||
if _, err := sess.Update(c); err != nil {
|
c.ContentVersion = contentVersion + 1
|
||||||
|
|
||||||
|
affected, err := sess.Where("content_version = ?", contentVersion).Update(c)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if affected == 0 {
|
||||||
|
return ErrCommentAlreadyChanged
|
||||||
|
}
|
||||||
if err := c.AddCrossReferences(ctx, doer, true); err != nil {
|
if err := c.AddCrossReferences(ctx, doer, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,25 +16,25 @@ import (
|
||||||
// CommentList defines a list of comments
|
// CommentList defines a list of comments
|
||||||
type CommentList []*Comment
|
type CommentList []*Comment
|
||||||
|
|
||||||
func (comments CommentList) getPosterIDs() []int64 {
|
|
||||||
return container.FilterSlice(comments, func(c *Comment) (int64, bool) {
|
|
||||||
return c.PosterID, c.PosterID > 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadPosters loads posters
|
// LoadPosters loads posters
|
||||||
func (comments CommentList) LoadPosters(ctx context.Context) error {
|
func (comments CommentList) LoadPosters(ctx context.Context) error {
|
||||||
if len(comments) == 0 {
|
if len(comments) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
posterMaps, err := getPosters(ctx, comments.getPosterIDs())
|
posterIDs := container.FilterSlice(comments, func(c *Comment) (int64, bool) {
|
||||||
|
return c.PosterID, c.Poster == nil && c.PosterID > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
posterMaps, err := getPostersByIDs(ctx, posterIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, comment := range comments {
|
for _, comment := range comments {
|
||||||
comment.Poster = getPoster(comment.PosterID, posterMaps)
|
if comment.Poster == nil {
|
||||||
|
comment.Poster = getPoster(comment.PosterID, posterMaps)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,33 +94,39 @@ func (err ErrIssueWasClosed) Error() string {
|
||||||
return fmt.Sprintf("Issue [%d] %d was already closed", err.ID, err.Index)
|
return fmt.Sprintf("Issue [%d] %d was already closed", err.ID, err.Index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already changed")
|
||||||
|
|
||||||
// Issue represents an issue or pull request of repository.
|
// Issue represents an issue or pull request of repository.
|
||||||
type Issue struct {
|
type Issue struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
|
RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
|
||||||
Repo *repo_model.Repository `xorm:"-"`
|
Repo *repo_model.Repository `xorm:"-"`
|
||||||
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
|
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
|
||||||
PosterID int64 `xorm:"INDEX"`
|
PosterID int64 `xorm:"INDEX"`
|
||||||
Poster *user_model.User `xorm:"-"`
|
Poster *user_model.User `xorm:"-"`
|
||||||
OriginalAuthor string
|
OriginalAuthor string
|
||||||
OriginalAuthorID int64 `xorm:"index"`
|
OriginalAuthorID int64 `xorm:"index"`
|
||||||
Title string `xorm:"name"`
|
Title string `xorm:"name"`
|
||||||
Content string `xorm:"LONGTEXT"`
|
Content string `xorm:"LONGTEXT"`
|
||||||
RenderedContent template.HTML `xorm:"-"`
|
RenderedContent template.HTML `xorm:"-"`
|
||||||
Labels []*Label `xorm:"-"`
|
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
||||||
MilestoneID int64 `xorm:"INDEX"`
|
Labels []*Label `xorm:"-"`
|
||||||
Milestone *Milestone `xorm:"-"`
|
isLabelsLoaded bool `xorm:"-"`
|
||||||
Project *project_model.Project `xorm:"-"`
|
MilestoneID int64 `xorm:"INDEX"`
|
||||||
Priority int
|
Milestone *Milestone `xorm:"-"`
|
||||||
AssigneeID int64 `xorm:"-"`
|
isMilestoneLoaded bool `xorm:"-"`
|
||||||
Assignee *user_model.User `xorm:"-"`
|
Project *project_model.Project `xorm:"-"`
|
||||||
IsClosed bool `xorm:"INDEX"`
|
Priority int
|
||||||
IsRead bool `xorm:"-"`
|
AssigneeID int64 `xorm:"-"`
|
||||||
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
Assignee *user_model.User `xorm:"-"`
|
||||||
PullRequest *PullRequest `xorm:"-"`
|
isAssigneeLoaded bool `xorm:"-"`
|
||||||
NumComments int
|
IsClosed bool `xorm:"INDEX"`
|
||||||
Ref string
|
IsRead bool `xorm:"-"`
|
||||||
PinOrder int `xorm:"DEFAULT 0"`
|
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
||||||
|
PullRequest *PullRequest `xorm:"-"`
|
||||||
|
NumComments int
|
||||||
|
Ref string
|
||||||
|
PinOrder int `xorm:"DEFAULT 0"`
|
||||||
|
|
||||||
DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`
|
DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||||
|
|
||||||
|
@ -131,11 +137,12 @@ type Issue struct {
|
||||||
ClosedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
ClosedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||||
NoAutoTime bool `xorm:"-"`
|
NoAutoTime bool `xorm:"-"`
|
||||||
|
|
||||||
Attachments []*repo_model.Attachment `xorm:"-"`
|
Attachments []*repo_model.Attachment `xorm:"-"`
|
||||||
Comments CommentList `xorm:"-"`
|
isAttachmentsLoaded bool `xorm:"-"`
|
||||||
Reactions ReactionList `xorm:"-"`
|
Comments CommentList `xorm:"-"`
|
||||||
TotalTrackedTime int64 `xorm:"-"`
|
Reactions ReactionList `xorm:"-"`
|
||||||
Assignees []*user_model.User `xorm:"-"`
|
TotalTrackedTime int64 `xorm:"-"`
|
||||||
|
Assignees []*user_model.User `xorm:"-"`
|
||||||
|
|
||||||
// IsLocked limits commenting abilities to users on an issue
|
// IsLocked limits commenting abilities to users on an issue
|
||||||
// with write access
|
// with write access
|
||||||
|
@ -187,6 +194,19 @@ func (issue *Issue) LoadRepo(ctx context.Context) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (issue *Issue) LoadAttachments(ctx context.Context) (err error) {
|
||||||
|
if issue.isAttachmentsLoaded || issue.Attachments != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)
|
||||||
|
}
|
||||||
|
issue.isAttachmentsLoaded = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// IsTimetrackerEnabled returns true if the repo enables timetracking
|
// IsTimetrackerEnabled returns true if the repo enables timetracking
|
||||||
func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
|
func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
|
||||||
if err := issue.LoadRepo(ctx); err != nil {
|
if err := issue.LoadRepo(ctx); err != nil {
|
||||||
|
@ -287,11 +307,12 @@ func (issue *Issue) loadReactions(ctx context.Context) (err error) {
|
||||||
|
|
||||||
// LoadMilestone load milestone of this issue.
|
// LoadMilestone load milestone of this issue.
|
||||||
func (issue *Issue) LoadMilestone(ctx context.Context) (err error) {
|
func (issue *Issue) LoadMilestone(ctx context.Context) (err error) {
|
||||||
if (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {
|
if !issue.isMilestoneLoaded && (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {
|
||||||
issue.Milestone, err = GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID)
|
issue.Milestone, err = GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID)
|
||||||
if err != nil && !IsErrMilestoneNotExist(err) {
|
if err != nil && !IsErrMilestoneNotExist(err) {
|
||||||
return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %w", issue.RepoID, issue.MilestoneID, err)
|
return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %w", issue.RepoID, issue.MilestoneID, err)
|
||||||
}
|
}
|
||||||
|
issue.isMilestoneLoaded = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -327,11 +348,8 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if issue.Attachments == nil {
|
if err = issue.LoadAttachments(ctx); err != nil {
|
||||||
issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)
|
return err
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = issue.loadComments(ctx); err != nil {
|
if err = issue.loadComments(ctx); err != nil {
|
||||||
|
@ -350,6 +368,13 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
|
||||||
return issue.loadReactions(ctx)
|
return issue.loadReactions(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (issue *Issue) ResetAttributesLoaded() {
|
||||||
|
issue.isLabelsLoaded = false
|
||||||
|
issue.isMilestoneLoaded = false
|
||||||
|
issue.isAttachmentsLoaded = false
|
||||||
|
issue.isAssigneeLoaded = false
|
||||||
|
}
|
||||||
|
|
||||||
// GetIsRead load the `IsRead` field of the issue
|
// GetIsRead load the `IsRead` field of the issue
|
||||||
func (issue *Issue) GetIsRead(ctx context.Context, userID int64) error {
|
func (issue *Issue) GetIsRead(ctx context.Context, userID int64) error {
|
||||||
issueUser := &IssueUser{IssueID: issue.ID, UID: userID}
|
issueUser := &IssueUser{IssueID: issue.ID, UID: userID}
|
||||||
|
|
|
@ -111,6 +111,7 @@ func NewIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_m
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
issue.isLabelsLoaded = false
|
||||||
issue.Labels = nil
|
issue.Labels = nil
|
||||||
if err = issue.LoadLabels(ctx); err != nil {
|
if err = issue.LoadLabels(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -160,6 +161,8 @@ func NewIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *us
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reload all labels
|
||||||
|
issue.isLabelsLoaded = false
|
||||||
issue.Labels = nil
|
issue.Labels = nil
|
||||||
if err = issue.LoadLabels(ctx); err != nil {
|
if err = issue.LoadLabels(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -325,11 +328,12 @@ func FixIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) {
|
||||||
|
|
||||||
// LoadLabels loads labels
|
// LoadLabels loads labels
|
||||||
func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
|
func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
|
||||||
if issue.Labels == nil && issue.ID != 0 {
|
if !issue.isLabelsLoaded && issue.Labels == nil && issue.ID != 0 {
|
||||||
issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
|
issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
|
return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
|
||||||
}
|
}
|
||||||
|
issue.isLabelsLoaded = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,29 +73,29 @@ func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.Reposi
|
||||||
return repo_model.ValuesRepository(repoMaps), nil
|
return repo_model.ValuesRepository(repoMaps), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (issues IssueList) getPosterIDs() []int64 {
|
func (issues IssueList) LoadPosters(ctx context.Context) error {
|
||||||
return container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
|
|
||||||
return issue.PosterID, true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (issues IssueList) loadPosters(ctx context.Context) error {
|
|
||||||
if len(issues) == 0 {
|
if len(issues) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
posterMaps, err := getPosters(ctx, issues.getPosterIDs())
|
posterIDs := container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
|
||||||
|
return issue.PosterID, issue.Poster == nil && issue.PosterID > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
posterMaps, err := getPostersByIDs(ctx, posterIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
issue.Poster = getPoster(issue.PosterID, posterMaps)
|
if issue.Poster == nil {
|
||||||
|
issue.Poster = getPoster(issue.PosterID, posterMaps)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPosters(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) {
|
func getPostersByIDs(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) {
|
||||||
posterMaps := make(map[int64]*user_model.User, len(posterIDs))
|
posterMaps := make(map[int64]*user_model.User, len(posterIDs))
|
||||||
left := len(posterIDs)
|
left := len(posterIDs)
|
||||||
for left > 0 {
|
for left > 0 {
|
||||||
|
@ -137,7 +137,7 @@ func (issues IssueList) getIssueIDs() []int64 {
|
||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
func (issues IssueList) loadLabels(ctx context.Context) error {
|
func (issues IssueList) LoadLabels(ctx context.Context) error {
|
||||||
if len(issues) == 0 {
|
if len(issues) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -169,7 +169,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
||||||
err = rows.Scan(&labelIssue)
|
err = rows.Scan(&labelIssue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err1 := rows.Close(); err1 != nil {
|
if err1 := rows.Close(); err1 != nil {
|
||||||
return fmt.Errorf("IssueList.loadLabels: Close: %w", err1)
|
return fmt.Errorf("IssueList.LoadLabels: Close: %w", err1)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -178,7 +178,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
||||||
// When there are no rows left and we try to close it.
|
// When there are no rows left and we try to close it.
|
||||||
// Since that is not relevant for us, we can safely ignore it.
|
// Since that is not relevant for us, we can safely ignore it.
|
||||||
if err1 := rows.Close(); err1 != nil {
|
if err1 := rows.Close(); err1 != nil {
|
||||||
return fmt.Errorf("IssueList.loadLabels: Close: %w", err1)
|
return fmt.Errorf("IssueList.LoadLabels: Close: %w", err1)
|
||||||
}
|
}
|
||||||
left -= limit
|
left -= limit
|
||||||
issueIDs = issueIDs[limit:]
|
issueIDs = issueIDs[limit:]
|
||||||
|
@ -186,6 +186,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
||||||
|
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
issue.Labels = issueLabels[issue.ID]
|
issue.Labels = issueLabels[issue.ID]
|
||||||
|
issue.isLabelsLoaded = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -196,7 +197,7 @@ func (issues IssueList) getMilestoneIDs() []int64 {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (issues IssueList) loadMilestones(ctx context.Context) error {
|
func (issues IssueList) LoadMilestones(ctx context.Context) error {
|
||||||
milestoneIDs := issues.getMilestoneIDs()
|
milestoneIDs := issues.getMilestoneIDs()
|
||||||
if len(milestoneIDs) == 0 {
|
if len(milestoneIDs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
@ -221,6 +222,7 @@ func (issues IssueList) loadMilestones(ctx context.Context) error {
|
||||||
|
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
issue.Milestone = milestoneMaps[issue.MilestoneID]
|
issue.Milestone = milestoneMaps[issue.MilestoneID]
|
||||||
|
issue.isMilestoneLoaded = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -264,7 +266,7 @@ func (issues IssueList) LoadProjects(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (issues IssueList) loadAssignees(ctx context.Context) error {
|
func (issues IssueList) LoadAssignees(ctx context.Context) error {
|
||||||
if len(issues) == 0 {
|
if len(issues) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -311,6 +313,10 @@ func (issues IssueList) loadAssignees(ctx context.Context) error {
|
||||||
|
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
issue.Assignees = assignees[issue.ID]
|
issue.Assignees = assignees[issue.ID]
|
||||||
|
if len(issue.Assignees) > 0 {
|
||||||
|
issue.Assignee = issue.Assignees[0]
|
||||||
|
}
|
||||||
|
issue.isAssigneeLoaded = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -414,6 +420,7 @@ func (issues IssueList) LoadAttachments(ctx context.Context) (err error) {
|
||||||
|
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
issue.Attachments = attachments[issue.ID]
|
issue.Attachments = attachments[issue.ID]
|
||||||
|
issue.isAttachmentsLoaded = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -539,23 +546,23 @@ func (issues IssueList) LoadAttributes(ctx context.Context) error {
|
||||||
return fmt.Errorf("issue.loadAttributes: LoadRepositories: %w", err)
|
return fmt.Errorf("issue.loadAttributes: LoadRepositories: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := issues.loadPosters(ctx); err != nil {
|
if err := issues.LoadPosters(ctx); err != nil {
|
||||||
return fmt.Errorf("issue.loadAttributes: loadPosters: %w", err)
|
return fmt.Errorf("issue.loadAttributes: LoadPosters: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := issues.loadLabels(ctx); err != nil {
|
if err := issues.LoadLabels(ctx); err != nil {
|
||||||
return fmt.Errorf("issue.loadAttributes: loadLabels: %w", err)
|
return fmt.Errorf("issue.loadAttributes: LoadLabels: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := issues.loadMilestones(ctx); err != nil {
|
if err := issues.LoadMilestones(ctx); err != nil {
|
||||||
return fmt.Errorf("issue.loadAttributes: loadMilestones: %w", err)
|
return fmt.Errorf("issue.loadAttributes: LoadMilestones: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := issues.LoadProjects(ctx); err != nil {
|
if err := issues.LoadProjects(ctx); err != nil {
|
||||||
return fmt.Errorf("issue.loadAttributes: loadProjects: %w", err)
|
return fmt.Errorf("issue.loadAttributes: loadProjects: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := issues.loadAssignees(ctx); err != nil {
|
if err := issues.LoadAssignees(ctx); err != nil {
|
||||||
return fmt.Errorf("issue.loadAttributes: loadAssignees: %w", err)
|
return fmt.Errorf("issue.loadAttributes: loadAssignees: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,22 +37,22 @@ func (issue *Issue) projectID(ctx context.Context) int64 {
|
||||||
return ip.ProjectID
|
return ip.ProjectID
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProjectBoardID return project board id if issue was assigned to one
|
// ProjectColumnID return project column id if issue was assigned to one
|
||||||
func (issue *Issue) ProjectBoardID(ctx context.Context) int64 {
|
func (issue *Issue) ProjectColumnID(ctx context.Context) int64 {
|
||||||
var ip project_model.ProjectIssue
|
var ip project_model.ProjectIssue
|
||||||
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
|
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
|
||||||
if err != nil || !has {
|
if err != nil || !has {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return ip.ProjectBoardID
|
return ip.ProjectColumnID
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadIssuesFromBoard load issues assigned to this board
|
// LoadIssuesFromColumn load issues assigned to this column
|
||||||
func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList, error) {
|
func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column) (IssueList, error) {
|
||||||
issueList, err := Issues(ctx, &IssuesOptions{
|
issueList, err := Issues(ctx, &IssuesOptions{
|
||||||
ProjectBoardID: b.ID,
|
ProjectColumnID: b.ID,
|
||||||
ProjectID: b.ProjectID,
|
ProjectID: b.ProjectID,
|
||||||
SortType: "project-column-sorting",
|
SortType: "project-column-sorting",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -60,9 +60,9 @@ func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList
|
||||||
|
|
||||||
if b.Default {
|
if b.Default {
|
||||||
issues, err := Issues(ctx, &IssuesOptions{
|
issues, err := Issues(ctx, &IssuesOptions{
|
||||||
ProjectBoardID: db.NoConditionID,
|
ProjectColumnID: db.NoConditionID,
|
||||||
ProjectID: b.ProjectID,
|
ProjectID: b.ProjectID,
|
||||||
SortType: "project-column-sorting",
|
SortType: "project-column-sorting",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -77,11 +77,11 @@ func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList
|
||||||
return issueList, nil
|
return issueList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadIssuesFromBoardList load issues assigned to the boards
|
// LoadIssuesFromColumnList load issues assigned to the columns
|
||||||
func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (map[int64]IssueList, error) {
|
func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList) (map[int64]IssueList, error) {
|
||||||
issuesMap := make(map[int64]IssueList, len(bs))
|
issuesMap := make(map[int64]IssueList, len(bs))
|
||||||
for i := range bs {
|
for i := range bs {
|
||||||
il, err := LoadIssuesFromBoard(ctx, bs[i])
|
il, err := LoadIssuesFromColumn(ctx, bs[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,7 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
|
||||||
return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID)
|
return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID)
|
||||||
}
|
}
|
||||||
if newColumnID == 0 {
|
if newColumnID == 0 {
|
||||||
newDefaultColumn, err := newProject.GetDefaultBoard(ctx)
|
newDefaultColumn, err := newProject.GetDefaultColumn(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -153,10 +153,10 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
|
||||||
}
|
}
|
||||||
newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
|
newSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
|
||||||
return db.Insert(ctx, &project_model.ProjectIssue{
|
return db.Insert(ctx, &project_model.ProjectIssue{
|
||||||
IssueID: issue.ID,
|
IssueID: issue.ID,
|
||||||
ProjectID: newProjectID,
|
ProjectID: newProjectID,
|
||||||
ProjectBoardID: newColumnID,
|
ProjectColumnID: newColumnID,
|
||||||
Sorting: newSorting,
|
Sorting: newSorting,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ type IssuesOptions struct { //nolint
|
||||||
SubscriberID int64
|
SubscriberID int64
|
||||||
MilestoneIDs []int64
|
MilestoneIDs []int64
|
||||||
ProjectID int64
|
ProjectID int64
|
||||||
ProjectBoardID int64
|
ProjectColumnID int64
|
||||||
IsClosed optional.Option[bool]
|
IsClosed optional.Option[bool]
|
||||||
IsPull optional.Option[bool]
|
IsPull optional.Option[bool]
|
||||||
LabelIDs []int64
|
LabelIDs []int64
|
||||||
|
@ -169,12 +169,12 @@ func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Sessio
|
||||||
return sess
|
return sess
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyProjectBoardCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
|
func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
|
||||||
// opts.ProjectBoardID == 0 means all project boards,
|
// opts.ProjectColumnID == 0 means all project columns,
|
||||||
// do not need to apply any condition
|
// do not need to apply any condition
|
||||||
if opts.ProjectBoardID > 0 {
|
if opts.ProjectColumnID > 0 {
|
||||||
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID}))
|
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectColumnID}))
|
||||||
} else if opts.ProjectBoardID == db.NoConditionID {
|
} else if opts.ProjectColumnID == db.NoConditionID {
|
||||||
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0}))
|
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0}))
|
||||||
}
|
}
|
||||||
return sess
|
return sess
|
||||||
|
@ -246,7 +246,7 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
|
||||||
|
|
||||||
applyProjectCondition(sess, opts)
|
applyProjectCondition(sess, opts)
|
||||||
|
|
||||||
applyProjectBoardCondition(sess, opts)
|
applyProjectColumnCondition(sess, opts)
|
||||||
|
|
||||||
if opts.IsPull.Has() {
|
if opts.IsPull.Has() {
|
||||||
sess.And("issue.is_pull=?", opts.IsPull.Value())
|
sess.And("issue.is_pull=?", opts.IsPull.Value())
|
||||||
|
|
|
@ -25,17 +25,18 @@ import (
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdateIssueCols updates cols of issue
|
|
||||||
func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error {
|
func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error {
|
||||||
|
_, err := UpdateIssueColsWithCond(ctx, issue, builder.NewCond(), cols...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateIssueColsWithCond(ctx context.Context, issue *Issue, cond builder.Cond, cols ...string) (int64, error) {
|
||||||
sess := db.GetEngine(ctx).ID(issue.ID)
|
sess := db.GetEngine(ctx).ID(issue.ID)
|
||||||
if issue.NoAutoTime {
|
if issue.NoAutoTime {
|
||||||
cols = append(cols, []string{"updated_unix"}...)
|
cols = append(cols, []string{"updated_unix"}...)
|
||||||
sess.NoAutoTime()
|
sess.NoAutoTime()
|
||||||
}
|
}
|
||||||
if _, err := sess.Cols(cols...).Update(issue); err != nil {
|
return sess.Cols(cols...).Where(cond).Update(issue)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func changeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) {
|
func changeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) {
|
||||||
|
@ -250,7 +251,7 @@ func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChangeIssueContent changes issue content, as the given user.
|
// ChangeIssueContent changes issue content, as the given user.
|
||||||
func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User, content string) (err error) {
|
func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User, content string, contentVersion int) (err error) {
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -269,10 +270,16 @@ func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User
|
||||||
}
|
}
|
||||||
|
|
||||||
issue.Content = content
|
issue.Content = content
|
||||||
|
issue.ContentVersion = contentVersion + 1
|
||||||
|
|
||||||
if err = UpdateIssueCols(ctx, issue, "content"); err != nil {
|
expectedContentVersion := builder.NewCond().And(builder.Eq{"content_version": contentVersion})
|
||||||
|
affected, err := UpdateIssueColsWithCond(ctx, issue, expectedContentVersion, "content", "content_version")
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("UpdateIssueCols: %w", err)
|
return fmt.Errorf("UpdateIssueCols: %w", err)
|
||||||
}
|
}
|
||||||
|
if affected == 0 {
|
||||||
|
return ErrIssueAlreadyChanged
|
||||||
|
}
|
||||||
|
|
||||||
historyDate := timeutil.TimeStampNow()
|
historyDate := timeutil.TimeStampNow()
|
||||||
if issue.NoAutoTime {
|
if issue.NoAutoTime {
|
||||||
|
|
|
@ -159,10 +159,11 @@ type PullRequest struct {
|
||||||
|
|
||||||
ChangedProtectedFiles []string `xorm:"TEXT JSON"`
|
ChangedProtectedFiles []string `xorm:"TEXT JSON"`
|
||||||
|
|
||||||
IssueID int64 `xorm:"INDEX"`
|
IssueID int64 `xorm:"INDEX"`
|
||||||
Issue *Issue `xorm:"-"`
|
Issue *Issue `xorm:"-"`
|
||||||
Index int64
|
Index int64
|
||||||
RequestedReviewers []*user_model.User `xorm:"-"`
|
RequestedReviewers []*user_model.User `xorm:"-"`
|
||||||
|
isRequestedReviewersLoaded bool `xorm:"-"`
|
||||||
|
|
||||||
HeadRepoID int64 `xorm:"INDEX"`
|
HeadRepoID int64 `xorm:"INDEX"`
|
||||||
HeadRepo *repo_model.Repository `xorm:"-"`
|
HeadRepo *repo_model.Repository `xorm:"-"`
|
||||||
|
@ -289,7 +290,7 @@ func (pr *PullRequest) LoadHeadRepo(ctx context.Context) (err error) {
|
||||||
|
|
||||||
// LoadRequestedReviewers loads the requested reviewers.
|
// LoadRequestedReviewers loads the requested reviewers.
|
||||||
func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
||||||
if len(pr.RequestedReviewers) > 0 {
|
if pr.isRequestedReviewersLoaded || len(pr.RequestedReviewers) > 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,10 +298,10 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = reviews.LoadReviewers(ctx); err != nil {
|
if err = reviews.LoadReviewers(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
pr.isRequestedReviewersLoaded = true
|
||||||
for _, review := range reviews {
|
for _, review := range reviews {
|
||||||
pr.RequestedReviewers = append(pr.RequestedReviewers, review.Reviewer)
|
pr.RequestedReviewers = append(pr.RequestedReviewers, review.Reviewer)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,10 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
@ -129,7 +131,7 @@ func GetPullRequestIDsByCheckStatus(ctx context.Context, status PullRequestStatu
|
||||||
}
|
}
|
||||||
|
|
||||||
// PullRequests returns all pull requests for a base Repo by the given conditions
|
// PullRequests returns all pull requests for a base Repo by the given conditions
|
||||||
func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) ([]*PullRequest, int64, error) {
|
func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) (PullRequestList, int64, error) {
|
||||||
if opts.Page <= 0 {
|
if opts.Page <= 0 {
|
||||||
opts.Page = 1
|
opts.Page = 1
|
||||||
}
|
}
|
||||||
|
@ -159,50 +161,93 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
|
||||||
// PullRequestList defines a list of pull requests
|
// PullRequestList defines a list of pull requests
|
||||||
type PullRequestList []*PullRequest
|
type PullRequestList []*PullRequest
|
||||||
|
|
||||||
func (prs PullRequestList) LoadAttributes(ctx context.Context) error {
|
func (prs PullRequestList) getRepositoryIDs() []int64 {
|
||||||
if len(prs) == 0 {
|
repoIDs := make(container.Set[int64])
|
||||||
return nil
|
for _, pr := range prs {
|
||||||
|
if pr.BaseRepo == nil && pr.BaseRepoID > 0 {
|
||||||
|
repoIDs.Add(pr.BaseRepoID)
|
||||||
|
}
|
||||||
|
if pr.HeadRepo == nil && pr.HeadRepoID > 0 {
|
||||||
|
repoIDs.Add(pr.HeadRepoID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return repoIDs.Values()
|
||||||
|
}
|
||||||
|
|
||||||
// Load issues.
|
func (prs PullRequestList) LoadRepositories(ctx context.Context) error {
|
||||||
issueIDs := prs.GetIssueIDs()
|
repoIDs := prs.getRepositoryIDs()
|
||||||
issues := make([]*Issue, 0, len(issueIDs))
|
reposMap := make(map[int64]*repo_model.Repository, len(repoIDs))
|
||||||
if err := db.GetEngine(ctx).
|
if err := db.GetEngine(ctx).
|
||||||
Where("id > 0").
|
In("id", repoIDs).
|
||||||
In("id", issueIDs).
|
Find(&reposMap); err != nil {
|
||||||
Find(&issues); err != nil {
|
return fmt.Errorf("find repos: %w", err)
|
||||||
return fmt.Errorf("find issues: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
set := make(map[int64]*Issue)
|
|
||||||
for i := range issues {
|
|
||||||
set[issues[i].ID] = issues[i]
|
|
||||||
}
|
}
|
||||||
for _, pr := range prs {
|
for _, pr := range prs {
|
||||||
pr.Issue = set[pr.IssueID]
|
if pr.BaseRepo == nil {
|
||||||
/*
|
pr.BaseRepo = reposMap[pr.BaseRepoID]
|
||||||
Old code:
|
}
|
||||||
pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync
|
if pr.HeadRepo == nil {
|
||||||
|
pr.HeadRepo = reposMap[pr.HeadRepoID]
|
||||||
It's worth panic because it's almost impossible to happen under normal use.
|
pr.isHeadRepoLoaded = true
|
||||||
But in integration testing, an asynchronous task could read a database that has been reset.
|
|
||||||
So returning an error would make more sense, let the caller has a choice to ignore it.
|
|
||||||
*/
|
|
||||||
if pr.Issue == nil {
|
|
||||||
return fmt.Errorf("issues and prs may be not in sync: cannot find issue %v for pr %v: %w", pr.IssueID, pr.ID, util.ErrNotExist)
|
|
||||||
}
|
}
|
||||||
pr.Issue.PullRequest = pr
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (prs PullRequestList) LoadAttributes(ctx context.Context) error {
|
||||||
|
if _, err := prs.LoadIssues(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prs PullRequestList) LoadIssues(ctx context.Context) (IssueList, error) {
|
||||||
|
if len(prs) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load issues.
|
||||||
|
issueIDs := prs.GetIssueIDs()
|
||||||
|
issues := make(map[int64]*Issue, len(issueIDs))
|
||||||
|
if err := db.GetEngine(ctx).
|
||||||
|
In("id", issueIDs).
|
||||||
|
Find(&issues); err != nil {
|
||||||
|
return nil, fmt.Errorf("find issues: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
issueList := make(IssueList, 0, len(prs))
|
||||||
|
for _, pr := range prs {
|
||||||
|
if pr.Issue == nil {
|
||||||
|
pr.Issue = issues[pr.IssueID]
|
||||||
|
/*
|
||||||
|
Old code:
|
||||||
|
pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync
|
||||||
|
|
||||||
|
It's worth panic because it's almost impossible to happen under normal use.
|
||||||
|
But in integration testing, an asynchronous task could read a database that has been reset.
|
||||||
|
So returning an error would make more sense, let the caller has a choice to ignore it.
|
||||||
|
*/
|
||||||
|
if pr.Issue == nil {
|
||||||
|
return nil, fmt.Errorf("issues and prs may be not in sync: cannot find issue %v for pr %v: %w", pr.IssueID, pr.ID, util.ErrNotExist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pr.Issue.PullRequest = pr
|
||||||
|
if pr.Issue.Repo == nil {
|
||||||
|
pr.Issue.Repo = pr.BaseRepo
|
||||||
|
}
|
||||||
|
issueList = append(issueList, pr.Issue)
|
||||||
|
}
|
||||||
|
return issueList, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetIssueIDs returns all issue ids
|
// GetIssueIDs returns all issue ids
|
||||||
func (prs PullRequestList) GetIssueIDs() []int64 {
|
func (prs PullRequestList) GetIssueIDs() []int64 {
|
||||||
issueIDs := make([]int64, 0, len(prs))
|
return container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) {
|
||||||
for i := range prs {
|
if pr.Issue == nil {
|
||||||
issueIDs = append(issueIDs, prs[i].IssueID)
|
return pr.IssueID, pr.IssueID > 0
|
||||||
}
|
}
|
||||||
return issueIDs
|
return 0, false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo
|
// HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"code.gitea.io/gitea/models/migrations/v1_20"
|
"code.gitea.io/gitea/models/migrations/v1_20"
|
||||||
"code.gitea.io/gitea/models/migrations/v1_21"
|
"code.gitea.io/gitea/models/migrations/v1_21"
|
||||||
"code.gitea.io/gitea/models/migrations/v1_22"
|
"code.gitea.io/gitea/models/migrations/v1_22"
|
||||||
|
"code.gitea.io/gitea/models/migrations/v1_23"
|
||||||
"code.gitea.io/gitea/models/migrations/v1_6"
|
"code.gitea.io/gitea/models/migrations/v1_6"
|
||||||
"code.gitea.io/gitea/models/migrations/v1_7"
|
"code.gitea.io/gitea/models/migrations/v1_7"
|
||||||
"code.gitea.io/gitea/models/migrations/v1_8"
|
"code.gitea.io/gitea/models/migrations/v1_8"
|
||||||
|
@ -589,6 +590,9 @@ var migrations = []Migration{
|
||||||
NewMigration("Drop wrongly created table o_auth2_application", v1_22.DropWronglyCreatedTable),
|
NewMigration("Drop wrongly created table o_auth2_application", v1_22.DropWronglyCreatedTable),
|
||||||
|
|
||||||
// Gitea 1.22.0-rc1 ends at 299
|
// Gitea 1.22.0-rc1 ends at 299
|
||||||
|
|
||||||
|
// v299 -> v300
|
||||||
|
NewMigration("Add content version to issue and comment table", v1_23.AddContentVersionToIssueAndComment),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current db version
|
// GetCurrentDBVersion returns the current db version
|
||||||
|
|
|
@ -60,7 +60,7 @@ func addObjectFormatNameToRepository(x *xorm.Engine) error {
|
||||||
|
|
||||||
// Here to catch weird edge-cases where column constraints above are
|
// Here to catch weird edge-cases where column constraints above are
|
||||||
// not applied by the DB backend
|
// not applied by the DB backend
|
||||||
_, err := x.Exec("UPDATE repository set object_format_name = 'sha1' WHERE object_format_name = '' or object_format_name IS NULL")
|
_, err := x.Exec("UPDATE `repository` set `object_format_name` = 'sha1' WHERE `object_format_name` = '' or `object_format_name` IS NULL")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
|
|
||||||
func Test_CheckProjectColumnsConsistency(t *testing.T) {
|
func Test_CheckProjectColumnsConsistency(t *testing.T) {
|
||||||
// Prepare and load the testing database
|
// Prepare and load the testing database
|
||||||
x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Board))
|
x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Column))
|
||||||
defer deferable()
|
defer deferable()
|
||||||
if x == nil || t.Failed() {
|
if x == nil || t.Failed() {
|
||||||
return
|
return
|
||||||
|
@ -23,22 +23,22 @@ func Test_CheckProjectColumnsConsistency(t *testing.T) {
|
||||||
|
|
||||||
assert.NoError(t, CheckProjectColumnsConsistency(x))
|
assert.NoError(t, CheckProjectColumnsConsistency(x))
|
||||||
|
|
||||||
// check if default board was added
|
// check if default column was added
|
||||||
var defaultBoard project.Board
|
var defaultColumn project.Column
|
||||||
has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultBoard)
|
has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultColumn)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, has)
|
assert.True(t, has)
|
||||||
assert.Equal(t, int64(1), defaultBoard.ProjectID)
|
assert.Equal(t, int64(1), defaultColumn.ProjectID)
|
||||||
assert.True(t, defaultBoard.Default)
|
assert.True(t, defaultColumn.Default)
|
||||||
|
|
||||||
// check if multiple defaults, previous were removed and last will be kept
|
// check if multiple defaults, previous were removed and last will be kept
|
||||||
expectDefaultBoard, err := project.GetBoard(db.DefaultContext, 2)
|
expectDefaultColumn, err := project.GetColumn(db.DefaultContext, 2)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(2), expectDefaultBoard.ProjectID)
|
assert.Equal(t, int64(2), expectDefaultColumn.ProjectID)
|
||||||
assert.False(t, expectDefaultBoard.Default)
|
assert.False(t, expectDefaultColumn.Default)
|
||||||
|
|
||||||
expectNonDefaultBoard, err := project.GetBoard(db.DefaultContext, 3)
|
expectNonDefaultColumn, err := project.GetColumn(db.DefaultContext, 3)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(2), expectNonDefaultBoard.ProjectID)
|
assert.Equal(t, int64(2), expectNonDefaultColumn.ProjectID)
|
||||||
assert.True(t, expectNonDefaultBoard.Default)
|
assert.True(t, expectNonDefaultColumn.Default)
|
||||||
}
|
}
|
||||||
|
|
18
models/migrations/v1_23/v299.go
Normal file
18
models/migrations/v1_23/v299.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_23 //nolint
|
||||||
|
|
||||||
|
import "xorm.io/xorm"
|
||||||
|
|
||||||
|
func AddContentVersionToIssueAndComment(x *xorm.Engine) error {
|
||||||
|
type Issue struct {
|
||||||
|
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Comment struct {
|
||||||
|
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.Sync(new(Comment), new(Issue))
|
||||||
|
}
|
|
@ -1,389 +0,0 @@
|
||||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package project
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
|
|
||||||
"xorm.io/builder"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
// BoardType is used to represent a project board type
|
|
||||||
BoardType uint8
|
|
||||||
|
|
||||||
// CardType is used to represent a project board card type
|
|
||||||
CardType uint8
|
|
||||||
|
|
||||||
// BoardList is a list of all project boards in a repository
|
|
||||||
BoardList []*Board
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// BoardTypeNone is a project board type that has no predefined columns
|
|
||||||
BoardTypeNone BoardType = iota
|
|
||||||
|
|
||||||
// BoardTypeBasicKanban is a project board type that has basic predefined columns
|
|
||||||
BoardTypeBasicKanban
|
|
||||||
|
|
||||||
// BoardTypeBugTriage is a project board type that has predefined columns suited to hunting down bugs
|
|
||||||
BoardTypeBugTriage
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// CardTypeTextOnly is a project board card type that is text only
|
|
||||||
CardTypeTextOnly CardType = iota
|
|
||||||
|
|
||||||
// CardTypeImagesAndText is a project board card type that has images and text
|
|
||||||
CardTypeImagesAndText
|
|
||||||
)
|
|
||||||
|
|
||||||
// BoardColorPattern is a regexp witch can validate BoardColor
|
|
||||||
var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
|
|
||||||
|
|
||||||
// Board is used to represent boards on a project
|
|
||||||
type Board struct {
|
|
||||||
ID int64 `xorm:"pk autoincr"`
|
|
||||||
Title string
|
|
||||||
Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board
|
|
||||||
Sorting int8 `xorm:"NOT NULL DEFAULT 0"`
|
|
||||||
Color string `xorm:"VARCHAR(7)"`
|
|
||||||
|
|
||||||
ProjectID int64 `xorm:"INDEX NOT NULL"`
|
|
||||||
CreatorID int64 `xorm:"NOT NULL"`
|
|
||||||
|
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
|
||||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName return the real table name
|
|
||||||
func (Board) TableName() string {
|
|
||||||
return "project_board"
|
|
||||||
}
|
|
||||||
|
|
||||||
// NumIssues return counter of all issues assigned to the board
|
|
||||||
func (b *Board) NumIssues(ctx context.Context) int {
|
|
||||||
c, err := db.GetEngine(ctx).Table("project_issue").
|
|
||||||
Where("project_id=?", b.ProjectID).
|
|
||||||
And("project_board_id=?", b.ID).
|
|
||||||
GroupBy("issue_id").
|
|
||||||
Cols("issue_id").
|
|
||||||
Count()
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return int(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Board) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
|
|
||||||
issues := make([]*ProjectIssue, 0, 5)
|
|
||||||
if err := db.GetEngine(ctx).Where("project_id=?", b.ProjectID).
|
|
||||||
And("project_board_id=?", b.ID).
|
|
||||||
OrderBy("sorting, id").
|
|
||||||
Find(&issues); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return issues, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
db.RegisterModel(new(Board))
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsBoardTypeValid checks if the project board type is valid
|
|
||||||
func IsBoardTypeValid(p BoardType) bool {
|
|
||||||
switch p {
|
|
||||||
case BoardTypeNone, BoardTypeBasicKanban, BoardTypeBugTriage:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsCardTypeValid checks if the project board card type is valid
|
|
||||||
func IsCardTypeValid(p CardType) bool {
|
|
||||||
switch p {
|
|
||||||
case CardTypeTextOnly, CardTypeImagesAndText:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createBoardsForProjectsType(ctx context.Context, project *Project) error {
|
|
||||||
var items []string
|
|
||||||
|
|
||||||
switch project.BoardType {
|
|
||||||
case BoardTypeBugTriage:
|
|
||||||
items = setting.Project.ProjectBoardBugTriageType
|
|
||||||
|
|
||||||
case BoardTypeBasicKanban:
|
|
||||||
items = setting.Project.ProjectBoardBasicKanbanType
|
|
||||||
case BoardTypeNone:
|
|
||||||
fallthrough
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
board := Board{
|
|
||||||
CreatedUnix: timeutil.TimeStampNow(),
|
|
||||||
CreatorID: project.CreatorID,
|
|
||||||
Title: "Backlog",
|
|
||||||
ProjectID: project.ID,
|
|
||||||
Default: true,
|
|
||||||
}
|
|
||||||
if err := db.Insert(ctx, board); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(items) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
boards := make([]Board, 0, len(items))
|
|
||||||
|
|
||||||
for _, v := range items {
|
|
||||||
boards = append(boards, Board{
|
|
||||||
CreatedUnix: timeutil.TimeStampNow(),
|
|
||||||
CreatorID: project.CreatorID,
|
|
||||||
Title: v,
|
|
||||||
ProjectID: project.ID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.Insert(ctx, boards)
|
|
||||||
}
|
|
||||||
|
|
||||||
// maxProjectColumns max columns allowed in a project, this should not bigger than 127
|
|
||||||
// because sorting is int8 in database
|
|
||||||
const maxProjectColumns = 20
|
|
||||||
|
|
||||||
// NewBoard adds a new project board to a given project
|
|
||||||
func NewBoard(ctx context.Context, board *Board) error {
|
|
||||||
if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) {
|
|
||||||
return fmt.Errorf("bad color code: %s", board.Color)
|
|
||||||
}
|
|
||||||
res := struct {
|
|
||||||
MaxSorting int64
|
|
||||||
ColumnCount int64
|
|
||||||
}{}
|
|
||||||
if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count").Table("project_board").
|
|
||||||
Where("project_id=?", board.ProjectID).Get(&res); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if res.ColumnCount >= maxProjectColumns {
|
|
||||||
return fmt.Errorf("NewBoard: maximum number of columns reached")
|
|
||||||
}
|
|
||||||
board.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0))
|
|
||||||
_, err := db.GetEngine(ctx).Insert(board)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteBoardByID removes all issues references to the project board.
|
|
||||||
func DeleteBoardByID(ctx context.Context, boardID int64) error {
|
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer committer.Close()
|
|
||||||
|
|
||||||
if err := deleteBoardByID(ctx, boardID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return committer.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteBoardByID(ctx context.Context, boardID int64) error {
|
|
||||||
board, err := GetBoard(ctx, boardID)
|
|
||||||
if err != nil {
|
|
||||||
if IsErrProjectBoardNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if board.Default {
|
|
||||||
return fmt.Errorf("deleteBoardByID: cannot delete default board")
|
|
||||||
}
|
|
||||||
|
|
||||||
// move all issues to the default column
|
|
||||||
project, err := GetProjectByID(ctx, board.ProjectID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defaultColumn, err := project.GetDefaultBoard(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = board.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := db.GetEngine(ctx).ID(board.ID).NoAutoCondition().Delete(board); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteBoardByProjectID(ctx context.Context, projectID int64) error {
|
|
||||||
_, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&Board{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBoard fetches the current board of a project
|
|
||||||
func GetBoard(ctx context.Context, boardID int64) (*Board, error) {
|
|
||||||
board := new(Board)
|
|
||||||
has, err := db.GetEngine(ctx).ID(boardID).Get(board)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if !has {
|
|
||||||
return nil, ErrProjectBoardNotExist{BoardID: boardID}
|
|
||||||
}
|
|
||||||
|
|
||||||
return board, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateBoard updates a project board
|
|
||||||
func UpdateBoard(ctx context.Context, board *Board) error {
|
|
||||||
var fieldToUpdate []string
|
|
||||||
|
|
||||||
if board.Sorting != 0 {
|
|
||||||
fieldToUpdate = append(fieldToUpdate, "sorting")
|
|
||||||
}
|
|
||||||
|
|
||||||
if board.Title != "" {
|
|
||||||
fieldToUpdate = append(fieldToUpdate, "title")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(board.Color) != 0 && !BoardColorPattern.MatchString(board.Color) {
|
|
||||||
return fmt.Errorf("bad color code: %s", board.Color)
|
|
||||||
}
|
|
||||||
fieldToUpdate = append(fieldToUpdate, "color")
|
|
||||||
|
|
||||||
_, err := db.GetEngine(ctx).ID(board.ID).Cols(fieldToUpdate...).Update(board)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBoards fetches all boards related to a project
|
|
||||||
func (p *Project) GetBoards(ctx context.Context) (BoardList, error) {
|
|
||||||
boards := make([]*Board, 0, 5)
|
|
||||||
if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&boards); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return boards, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDefaultBoard return default board and ensure only one exists
|
|
||||||
func (p *Project) GetDefaultBoard(ctx context.Context) (*Board, error) {
|
|
||||||
var board Board
|
|
||||||
has, err := db.GetEngine(ctx).
|
|
||||||
Where("project_id=? AND `default` = ?", p.ID, true).
|
|
||||||
Desc("id").Get(&board)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if has {
|
|
||||||
return &board, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a default board if none is found
|
|
||||||
board = Board{
|
|
||||||
ProjectID: p.ID,
|
|
||||||
Default: true,
|
|
||||||
Title: "Uncategorized",
|
|
||||||
CreatorID: p.CreatorID,
|
|
||||||
}
|
|
||||||
if _, err := db.GetEngine(ctx).Insert(&board); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &board, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDefaultBoard represents a board for issues not assigned to one
|
|
||||||
func SetDefaultBoard(ctx context.Context, projectID, boardID int64) error {
|
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
||||||
if _, err := GetBoard(ctx, boardID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := db.GetEngine(ctx).Where(builder.Eq{
|
|
||||||
"project_id": projectID,
|
|
||||||
"`default`": true,
|
|
||||||
}).Cols("`default`").Update(&Board{Default: false}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := db.GetEngine(ctx).ID(boardID).
|
|
||||||
Where(builder.Eq{"project_id": projectID}).
|
|
||||||
Cols("`default`").Update(&Board{Default: true})
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateBoardSorting update project board sorting
|
|
||||||
func UpdateBoardSorting(ctx context.Context, bs BoardList) error {
|
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
||||||
for i := range bs {
|
|
||||||
if _, err := db.GetEngine(ctx).ID(bs[i].ID).Cols(
|
|
||||||
"sorting",
|
|
||||||
).Update(bs[i]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (BoardList, error) {
|
|
||||||
columns := make([]*Board, 0, 5)
|
|
||||||
if err := db.GetEngine(ctx).
|
|
||||||
Where("project_id =?", projectID).
|
|
||||||
In("id", columnsIDs).
|
|
||||||
OrderBy("sorting").Find(&columns); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return columns, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoveColumnsOnProject sorts columns in a project
|
|
||||||
func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error {
|
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
|
||||||
sess := db.GetEngine(ctx)
|
|
||||||
columnIDs := util.ValuesOfMap(sortedColumnIDs)
|
|
||||||
movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(movedColumns) != len(sortedColumnIDs) {
|
|
||||||
return errors.New("some columns do not exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, column := range movedColumns {
|
|
||||||
if column.ProjectID != project.ID {
|
|
||||||
return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for sorting, columnID := range sortedColumnIDs {
|
|
||||||
if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
359
models/project/column.go
Normal file
359
models/project/column.go
Normal file
|
@ -0,0 +1,359 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package project
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
|
||||||
|
// CardType is used to represent a project column card type
|
||||||
|
CardType uint8
|
||||||
|
|
||||||
|
// ColumnList is a list of all project columns in a repository
|
||||||
|
ColumnList []*Column
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// CardTypeTextOnly is a project column card type that is text only
|
||||||
|
CardTypeTextOnly CardType = iota
|
||||||
|
|
||||||
|
// CardTypeImagesAndText is a project column card type that has images and text
|
||||||
|
CardTypeImagesAndText
|
||||||
|
)
|
||||||
|
|
||||||
|
// ColumnColorPattern is a regexp witch can validate ColumnColor
|
||||||
|
var ColumnColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
|
||||||
|
|
||||||
|
// Column is used to represent column on a project
|
||||||
|
type Column struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
Title string
|
||||||
|
Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific column will be assigned to this column
|
||||||
|
Sorting int8 `xorm:"NOT NULL DEFAULT 0"`
|
||||||
|
Color string `xorm:"VARCHAR(7)"`
|
||||||
|
|
||||||
|
ProjectID int64 `xorm:"INDEX NOT NULL"`
|
||||||
|
CreatorID int64 `xorm:"NOT NULL"`
|
||||||
|
|
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||||
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName return the real table name
|
||||||
|
func (Column) TableName() string {
|
||||||
|
return "project_board" // TODO: the legacy table name should be project_column
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumIssues return counter of all issues assigned to the column
|
||||||
|
func (c *Column) NumIssues(ctx context.Context) int {
|
||||||
|
total, err := db.GetEngine(ctx).Table("project_issue").
|
||||||
|
Where("project_id=?", c.ProjectID).
|
||||||
|
And("project_board_id=?", c.ID).
|
||||||
|
GroupBy("issue_id").
|
||||||
|
Cols("issue_id").
|
||||||
|
Count()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
|
||||||
|
issues := make([]*ProjectIssue, 0, 5)
|
||||||
|
if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID).
|
||||||
|
And("project_board_id=?", c.ID).
|
||||||
|
OrderBy("sorting, id").
|
||||||
|
Find(&issues); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return issues, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
db.RegisterModel(new(Column))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCardTypeValid checks if the project column card type is valid
|
||||||
|
func IsCardTypeValid(p CardType) bool {
|
||||||
|
switch p {
|
||||||
|
case CardTypeTextOnly, CardTypeImagesAndText:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDefaultColumnsForProject(ctx context.Context, project *Project) error {
|
||||||
|
var items []string
|
||||||
|
|
||||||
|
switch project.TemplateType {
|
||||||
|
case TemplateTypeBugTriage:
|
||||||
|
items = setting.Project.ProjectBoardBugTriageType
|
||||||
|
case TemplateTypeBasicKanban:
|
||||||
|
items = setting.Project.ProjectBoardBasicKanbanType
|
||||||
|
case TemplateTypeNone:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
column := Column{
|
||||||
|
CreatedUnix: timeutil.TimeStampNow(),
|
||||||
|
CreatorID: project.CreatorID,
|
||||||
|
Title: "Backlog",
|
||||||
|
ProjectID: project.ID,
|
||||||
|
Default: true,
|
||||||
|
}
|
||||||
|
if err := db.Insert(ctx, column); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
columns := make([]Column, 0, len(items))
|
||||||
|
for _, v := range items {
|
||||||
|
columns = append(columns, Column{
|
||||||
|
CreatedUnix: timeutil.TimeStampNow(),
|
||||||
|
CreatorID: project.CreatorID,
|
||||||
|
Title: v,
|
||||||
|
ProjectID: project.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Insert(ctx, columns)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxProjectColumns max columns allowed in a project, this should not bigger than 127
|
||||||
|
// because sorting is int8 in database
|
||||||
|
const maxProjectColumns = 20
|
||||||
|
|
||||||
|
// NewColumn adds a new project column to a given project
|
||||||
|
func NewColumn(ctx context.Context, column *Column) error {
|
||||||
|
if len(column.Color) != 0 && !ColumnColorPattern.MatchString(column.Color) {
|
||||||
|
return fmt.Errorf("bad color code: %s", column.Color)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := struct {
|
||||||
|
MaxSorting int64
|
||||||
|
ColumnCount int64
|
||||||
|
}{}
|
||||||
|
if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count").Table("project_board").
|
||||||
|
Where("project_id=?", column.ProjectID).Get(&res); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if res.ColumnCount >= maxProjectColumns {
|
||||||
|
return fmt.Errorf("NewBoard: maximum number of columns reached")
|
||||||
|
}
|
||||||
|
column.Sorting = int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0))
|
||||||
|
_, err := db.GetEngine(ctx).Insert(column)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteColumnByID removes all issues references to the project column.
|
||||||
|
func DeleteColumnByID(ctx context.Context, columnID int64) error {
|
||||||
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
return deleteColumnByID(ctx, columnID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteColumnByID(ctx context.Context, columnID int64) error {
|
||||||
|
column, err := GetColumn(ctx, columnID)
|
||||||
|
if err != nil {
|
||||||
|
if IsErrProjectColumnNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if column.Default {
|
||||||
|
return fmt.Errorf("deleteColumnByID: cannot delete default column")
|
||||||
|
}
|
||||||
|
|
||||||
|
// move all issues to the default column
|
||||||
|
project, err := GetProjectByID(ctx, column.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defaultColumn, err := project.GetDefaultColumn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = column.moveIssuesToAnotherColumn(ctx, defaultColumn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := db.GetEngine(ctx).ID(column.ID).NoAutoCondition().Delete(column); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteColumnByProjectID(ctx context.Context, projectID int64) error {
|
||||||
|
_, err := db.GetEngine(ctx).Where("project_id=?", projectID).Delete(&Column{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColumn fetches the current column of a project
|
||||||
|
func GetColumn(ctx context.Context, columnID int64) (*Column, error) {
|
||||||
|
column := new(Column)
|
||||||
|
has, err := db.GetEngine(ctx).ID(columnID).Get(column)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, ErrProjectColumnNotExist{ColumnID: columnID}
|
||||||
|
}
|
||||||
|
|
||||||
|
return column, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateColumn updates a project column
|
||||||
|
func UpdateColumn(ctx context.Context, column *Column) error {
|
||||||
|
var fieldToUpdate []string
|
||||||
|
|
||||||
|
if column.Sorting != 0 {
|
||||||
|
fieldToUpdate = append(fieldToUpdate, "sorting")
|
||||||
|
}
|
||||||
|
|
||||||
|
if column.Title != "" {
|
||||||
|
fieldToUpdate = append(fieldToUpdate, "title")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(column.Color) != 0 && !ColumnColorPattern.MatchString(column.Color) {
|
||||||
|
return fmt.Errorf("bad color code: %s", column.Color)
|
||||||
|
}
|
||||||
|
fieldToUpdate = append(fieldToUpdate, "color")
|
||||||
|
|
||||||
|
_, err := db.GetEngine(ctx).ID(column.ID).Cols(fieldToUpdate...).Update(column)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColumns fetches all columns related to a project
|
||||||
|
func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) {
|
||||||
|
columns := make([]*Column, 0, 5)
|
||||||
|
if err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Find(&columns); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultColumn return default column and ensure only one exists
|
||||||
|
func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) {
|
||||||
|
var column Column
|
||||||
|
has, err := db.GetEngine(ctx).
|
||||||
|
Where("project_id=? AND `default` = ?", p.ID, true).
|
||||||
|
Desc("id").Get(&column)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if has {
|
||||||
|
return &column, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a default column if none is found
|
||||||
|
column = Column{
|
||||||
|
ProjectID: p.ID,
|
||||||
|
Default: true,
|
||||||
|
Title: "Uncategorized",
|
||||||
|
CreatorID: p.CreatorID,
|
||||||
|
}
|
||||||
|
if _, err := db.GetEngine(ctx).Insert(&column); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &column, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaultColumn represents a column for issues not assigned to one
|
||||||
|
func SetDefaultColumn(ctx context.Context, projectID, columnID int64) error {
|
||||||
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
if _, err := GetColumn(ctx, columnID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := db.GetEngine(ctx).Where(builder.Eq{
|
||||||
|
"project_id": projectID,
|
||||||
|
"`default`": true,
|
||||||
|
}).Cols("`default`").Update(&Column{Default: false}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := db.GetEngine(ctx).ID(columnID).
|
||||||
|
Where(builder.Eq{"project_id": projectID}).
|
||||||
|
Cols("`default`").Update(&Column{Default: true})
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateColumnSorting update project column sorting
|
||||||
|
func UpdateColumnSorting(ctx context.Context, cl ColumnList) error {
|
||||||
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
for i := range cl {
|
||||||
|
if _, err := db.GetEngine(ctx).ID(cl[i].ID).Cols(
|
||||||
|
"sorting",
|
||||||
|
).Update(cl[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) (ColumnList, error) {
|
||||||
|
columns := make([]*Column, 0, 5)
|
||||||
|
if err := db.GetEngine(ctx).
|
||||||
|
Where("project_id =?", projectID).
|
||||||
|
In("id", columnsIDs).
|
||||||
|
OrderBy("sorting").Find(&columns); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return columns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveColumnsOnProject sorts columns in a project
|
||||||
|
func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error {
|
||||||
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
|
sess := db.GetEngine(ctx)
|
||||||
|
columnIDs := util.ValuesOfMap(sortedColumnIDs)
|
||||||
|
movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(movedColumns) != len(sortedColumnIDs) {
|
||||||
|
return errors.New("some columns do not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, column := range movedColumns {
|
||||||
|
if column.ProjectID != project.ID {
|
||||||
|
return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for sorting, columnID := range sortedColumnIDs {
|
||||||
|
if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
|
@ -14,48 +14,48 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetDefaultBoard(t *testing.T) {
|
func TestGetDefaultColumn(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
projectWithoutDefault, err := GetProjectByID(db.DefaultContext, 5)
|
projectWithoutDefault, err := GetProjectByID(db.DefaultContext, 5)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// check if default board was added
|
// check if default column was added
|
||||||
board, err := projectWithoutDefault.GetDefaultBoard(db.DefaultContext)
|
column, err := projectWithoutDefault.GetDefaultColumn(db.DefaultContext)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(5), board.ProjectID)
|
assert.Equal(t, int64(5), column.ProjectID)
|
||||||
assert.Equal(t, "Uncategorized", board.Title)
|
assert.Equal(t, "Uncategorized", column.Title)
|
||||||
|
|
||||||
projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6)
|
projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// check if multiple defaults were removed
|
// check if multiple defaults were removed
|
||||||
board, err = projectWithMultipleDefaults.GetDefaultBoard(db.DefaultContext)
|
column, err = projectWithMultipleDefaults.GetDefaultColumn(db.DefaultContext)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(6), board.ProjectID)
|
assert.Equal(t, int64(6), column.ProjectID)
|
||||||
assert.Equal(t, int64(9), board.ID)
|
assert.Equal(t, int64(9), column.ID)
|
||||||
|
|
||||||
// set 8 as default board
|
// set 8 as default column
|
||||||
assert.NoError(t, SetDefaultBoard(db.DefaultContext, board.ProjectID, 8))
|
assert.NoError(t, SetDefaultColumn(db.DefaultContext, column.ProjectID, 8))
|
||||||
|
|
||||||
// then 9 will become a non-default board
|
// then 9 will become a non-default column
|
||||||
board, err = GetBoard(db.DefaultContext, 9)
|
column, err = GetColumn(db.DefaultContext, 9)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(6), board.ProjectID)
|
assert.Equal(t, int64(6), column.ProjectID)
|
||||||
assert.False(t, board.Default)
|
assert.False(t, column.Default)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_moveIssuesToAnotherColumn(t *testing.T) {
|
func Test_moveIssuesToAnotherColumn(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
column1 := unittest.AssertExistsAndLoadBean(t, &Board{ID: 1, ProjectID: 1})
|
column1 := unittest.AssertExistsAndLoadBean(t, &Column{ID: 1, ProjectID: 1})
|
||||||
|
|
||||||
issues, err := column1.GetIssues(db.DefaultContext)
|
issues, err := column1.GetIssues(db.DefaultContext)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, issues, 1)
|
assert.Len(t, issues, 1)
|
||||||
assert.EqualValues(t, 1, issues[0].ID)
|
assert.EqualValues(t, 1, issues[0].ID)
|
||||||
|
|
||||||
column2 := unittest.AssertExistsAndLoadBean(t, &Board{ID: 2, ProjectID: 1})
|
column2 := unittest.AssertExistsAndLoadBean(t, &Column{ID: 2, ProjectID: 1})
|
||||||
issues, err = column2.GetIssues(db.DefaultContext)
|
issues, err = column2.GetIssues(db.DefaultContext)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, issues, 1)
|
assert.Len(t, issues, 1)
|
||||||
|
@ -81,7 +81,7 @@ func Test_MoveColumnsOnProject(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
|
project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
|
||||||
columns, err := project1.GetBoards(db.DefaultContext)
|
columns, err := project1.GetColumns(db.DefaultContext)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, columns, 3)
|
assert.Len(t, columns, 3)
|
||||||
assert.EqualValues(t, 0, columns[0].Sorting) // even if there is no default sorting, the code should also work
|
assert.EqualValues(t, 0, columns[0].Sorting) // even if there is no default sorting, the code should also work
|
||||||
|
@ -95,7 +95,7 @@ func Test_MoveColumnsOnProject(t *testing.T) {
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
columnsAfter, err := project1.GetBoards(db.DefaultContext)
|
columnsAfter, err := project1.GetColumns(db.DefaultContext)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, columnsAfter, 3)
|
assert.Len(t, columnsAfter, 3)
|
||||||
assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID)
|
assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID)
|
||||||
|
@ -103,23 +103,23 @@ func Test_MoveColumnsOnProject(t *testing.T) {
|
||||||
assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID)
|
assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_NewBoard(t *testing.T) {
|
func Test_NewColumn(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
|
project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
|
||||||
columns, err := project1.GetBoards(db.DefaultContext)
|
columns, err := project1.GetColumns(db.DefaultContext)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, columns, 3)
|
assert.Len(t, columns, 3)
|
||||||
|
|
||||||
for i := 0; i < maxProjectColumns-3; i++ {
|
for i := 0; i < maxProjectColumns-3; i++ {
|
||||||
err := NewBoard(db.DefaultContext, &Board{
|
err := NewColumn(db.DefaultContext, &Column{
|
||||||
Title: fmt.Sprintf("board-%d", i+4),
|
Title: fmt.Sprintf("column-%d", i+4),
|
||||||
ProjectID: project1.ID,
|
ProjectID: project1.ID,
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
err = NewBoard(db.DefaultContext, &Board{
|
err = NewColumn(db.DefaultContext, &Column{
|
||||||
Title: "board-21",
|
Title: "column-21",
|
||||||
ProjectID: project1.ID,
|
ProjectID: project1.ID,
|
||||||
})
|
})
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
|
@ -18,10 +18,10 @@ type ProjectIssue struct { //revive:disable-line:exported
|
||||||
IssueID int64 `xorm:"INDEX"`
|
IssueID int64 `xorm:"INDEX"`
|
||||||
ProjectID int64 `xorm:"INDEX"`
|
ProjectID int64 `xorm:"INDEX"`
|
||||||
|
|
||||||
// ProjectBoardID should not be zero since 1.22. If it's zero, the issue will not be displayed on UI and it might result in errors.
|
// ProjectColumnID should not be zero since 1.22. If it's zero, the issue will not be displayed on UI and it might result in errors.
|
||||||
ProjectBoardID int64 `xorm:"INDEX"`
|
ProjectColumnID int64 `xorm:"'project_board_id' INDEX"`
|
||||||
|
|
||||||
// the sorting order on the board
|
// the sorting order on the column
|
||||||
Sorting int64 `xorm:"NOT NULL DEFAULT 0"`
|
Sorting int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,13 +76,13 @@ func (p *Project) NumOpenIssues(ctx context.Context) int {
|
||||||
return int(c)
|
return int(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MoveIssuesOnProjectBoard moves or keeps issues in a column and sorts them inside that column
|
// MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column
|
||||||
func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs map[int64]int64) error {
|
func MoveIssuesOnProjectColumn(ctx context.Context, column *Column, sortedIssueIDs map[int64]int64) error {
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
sess := db.GetEngine(ctx)
|
sess := db.GetEngine(ctx)
|
||||||
issueIDs := util.ValuesOfMap(sortedIssueIDs)
|
issueIDs := util.ValuesOfMap(sortedIssueIDs)
|
||||||
|
|
||||||
count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", board.ProjectID).In("issue_id", issueIDs).Count()
|
count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", column.ProjectID).In("issue_id", issueIDs).Count()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,7 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
for sorting, issueID := range sortedIssueIDs {
|
for sorting, issueID := range sortedIssueIDs {
|
||||||
_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", board.ID, sorting, issueID)
|
_, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -100,12 +100,12 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board) error {
|
func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
|
||||||
if b.ProjectID != newColumn.ProjectID {
|
if c.ProjectID != newColumn.ProjectID {
|
||||||
return fmt.Errorf("columns have to be in the same project")
|
return fmt.Errorf("columns have to be in the same project")
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.ID == newColumn.ID {
|
if c.ID == newColumn.ID {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
issues, err := b.GetIssues(ctx)
|
issues, err := c.GetIssues(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,7 @@ func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board)
|
||||||
nextSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
|
nextSorting := util.Iif(res.IssueCount > 0, res.MaxSorting+1, 0)
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
for i, issue := range issues {
|
for i, issue := range issues {
|
||||||
issue.ProjectBoardID = newColumn.ID
|
issue.ProjectColumnID = newColumn.ID
|
||||||
issue.Sorting = nextSorting + int64(i)
|
issue.Sorting = nextSorting + int64(i)
|
||||||
if _, err := db.GetEngine(ctx).ID(issue.ID).Cols("project_board_id", "sorting").Update(issue); err != nil {
|
if _, err := db.GetEngine(ctx).ID(issue.ID).Cols("project_board_id", "sorting").Update(issue); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -21,13 +21,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// BoardConfig is used to identify the type of board that is being created
|
// CardConfig is used to identify the type of column card that is being used
|
||||||
BoardConfig struct {
|
|
||||||
BoardType BoardType
|
|
||||||
Translation string
|
|
||||||
}
|
|
||||||
|
|
||||||
// CardConfig is used to identify the type of board card that is being used
|
|
||||||
CardConfig struct {
|
CardConfig struct {
|
||||||
CardType CardType
|
CardType CardType
|
||||||
Translation string
|
Translation string
|
||||||
|
@ -38,7 +32,7 @@ type (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// TypeIndividual is a type of project board that is owned by an individual
|
// TypeIndividual is a type of project column that is owned by an individual
|
||||||
TypeIndividual Type = iota + 1
|
TypeIndividual Type = iota + 1
|
||||||
|
|
||||||
// TypeRepository is a project that is tied to a repository
|
// TypeRepository is a project that is tied to a repository
|
||||||
|
@ -68,39 +62,39 @@ func (err ErrProjectNotExist) Unwrap() error {
|
||||||
return util.ErrNotExist
|
return util.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrProjectBoardNotExist represents a "ProjectBoardNotExist" kind of error.
|
// ErrProjectColumnNotExist represents a "ErrProjectColumnNotExist" kind of error.
|
||||||
type ErrProjectBoardNotExist struct {
|
type ErrProjectColumnNotExist struct {
|
||||||
BoardID int64
|
ColumnID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrProjectBoardNotExist checks if an error is a ErrProjectBoardNotExist
|
// IsErrProjectColumnNotExist checks if an error is a ErrProjectColumnNotExist
|
||||||
func IsErrProjectBoardNotExist(err error) bool {
|
func IsErrProjectColumnNotExist(err error) bool {
|
||||||
_, ok := err.(ErrProjectBoardNotExist)
|
_, ok := err.(ErrProjectColumnNotExist)
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrProjectBoardNotExist) Error() string {
|
func (err ErrProjectColumnNotExist) Error() string {
|
||||||
return fmt.Sprintf("project board does not exist [id: %d]", err.BoardID)
|
return fmt.Sprintf("project column does not exist [id: %d]", err.ColumnID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrProjectBoardNotExist) Unwrap() error {
|
func (err ErrProjectColumnNotExist) Unwrap() error {
|
||||||
return util.ErrNotExist
|
return util.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project represents a project board
|
// Project represents a project
|
||||||
type Project struct {
|
type Project struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
Title string `xorm:"INDEX NOT NULL"`
|
Title string `xorm:"INDEX NOT NULL"`
|
||||||
Description string `xorm:"TEXT"`
|
Description string `xorm:"TEXT"`
|
||||||
OwnerID int64 `xorm:"INDEX"`
|
OwnerID int64 `xorm:"INDEX"`
|
||||||
Owner *user_model.User `xorm:"-"`
|
Owner *user_model.User `xorm:"-"`
|
||||||
RepoID int64 `xorm:"INDEX"`
|
RepoID int64 `xorm:"INDEX"`
|
||||||
Repo *repo_model.Repository `xorm:"-"`
|
Repo *repo_model.Repository `xorm:"-"`
|
||||||
CreatorID int64 `xorm:"NOT NULL"`
|
CreatorID int64 `xorm:"NOT NULL"`
|
||||||
IsClosed bool `xorm:"INDEX"`
|
IsClosed bool `xorm:"INDEX"`
|
||||||
BoardType BoardType
|
TemplateType TemplateType `xorm:"'board_type'"` // TODO: rename the column to template_type
|
||||||
CardType CardType
|
CardType CardType
|
||||||
Type Type
|
Type Type
|
||||||
|
|
||||||
RenderedContent template.HTML `xorm:"-"`
|
RenderedContent template.HTML `xorm:"-"`
|
||||||
|
|
||||||
|
@ -172,16 +166,7 @@ func init() {
|
||||||
db.RegisterModel(new(Project))
|
db.RegisterModel(new(Project))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBoardConfig retrieves the types of configurations project boards could have
|
// GetCardConfig retrieves the types of configurations project column cards could have
|
||||||
func GetBoardConfig() []BoardConfig {
|
|
||||||
return []BoardConfig{
|
|
||||||
{BoardTypeNone, "repo.projects.type.none"},
|
|
||||||
{BoardTypeBasicKanban, "repo.projects.type.basic_kanban"},
|
|
||||||
{BoardTypeBugTriage, "repo.projects.type.bug_triage"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCardConfig retrieves the types of configurations project board cards could have
|
|
||||||
func GetCardConfig() []CardConfig {
|
func GetCardConfig() []CardConfig {
|
||||||
return []CardConfig{
|
return []CardConfig{
|
||||||
{CardTypeTextOnly, "repo.projects.card_type.text_only"},
|
{CardTypeTextOnly, "repo.projects.card_type.text_only"},
|
||||||
|
@ -251,8 +236,8 @@ func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy {
|
||||||
|
|
||||||
// NewProject creates a new Project
|
// NewProject creates a new Project
|
||||||
func NewProject(ctx context.Context, p *Project) error {
|
func NewProject(ctx context.Context, p *Project) error {
|
||||||
if !IsBoardTypeValid(p.BoardType) {
|
if !IsTemplateTypeValid(p.TemplateType) {
|
||||||
p.BoardType = BoardTypeNone
|
p.TemplateType = TemplateTypeNone
|
||||||
}
|
}
|
||||||
|
|
||||||
if !IsCardTypeValid(p.CardType) {
|
if !IsCardTypeValid(p.CardType) {
|
||||||
|
@ -263,27 +248,19 @@ func NewProject(ctx context.Context, p *Project) error {
|
||||||
return util.NewInvalidArgumentErrorf("project type is not valid")
|
return util.NewInvalidArgumentErrorf("project type is not valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
if err != nil {
|
if err := db.Insert(ctx, p); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer committer.Close()
|
|
||||||
|
|
||||||
if err := db.Insert(ctx, p); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.RepoID > 0 {
|
|
||||||
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if err := createBoardsForProjectsType(ctx, p); err != nil {
|
if p.RepoID > 0 {
|
||||||
return err
|
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_projects = num_projects + 1 WHERE id = ?", p.RepoID); err != nil {
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return committer.Commit()
|
return createDefaultColumnsForProject(ctx, p)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProjectByID returns the projects in a repository
|
// GetProjectByID returns the projects in a repository
|
||||||
|
@ -417,7 +394,7 @@ func DeleteProjectByID(ctx context.Context, id int64) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := deleteBoardByProjectID(ctx, id); err != nil {
|
if err := deleteColumnByProjectID(ctx, id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,13 +51,13 @@ func TestProject(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
project := &Project{
|
project := &Project{
|
||||||
Type: TypeRepository,
|
Type: TypeRepository,
|
||||||
BoardType: BoardTypeBasicKanban,
|
TemplateType: TemplateTypeBasicKanban,
|
||||||
CardType: CardTypeTextOnly,
|
CardType: CardTypeTextOnly,
|
||||||
Title: "New Project",
|
Title: "New Project",
|
||||||
RepoID: 1,
|
RepoID: 1,
|
||||||
CreatedUnix: timeutil.TimeStampNow(),
|
CreatedUnix: timeutil.TimeStampNow(),
|
||||||
CreatorID: 2,
|
CreatorID: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.NoError(t, NewProject(db.DefaultContext, project))
|
assert.NoError(t, NewProject(db.DefaultContext, project))
|
||||||
|
|
45
models/project/template.go
Normal file
45
models/project/template.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package project
|
||||||
|
|
||||||
|
type (
|
||||||
|
// TemplateType is used to represent a project template type
|
||||||
|
TemplateType uint8
|
||||||
|
|
||||||
|
// TemplateConfig is used to identify the template type of project that is being created
|
||||||
|
TemplateConfig struct {
|
||||||
|
TemplateType TemplateType
|
||||||
|
Translation string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TemplateTypeNone is a project template type that has no predefined columns
|
||||||
|
TemplateTypeNone TemplateType = iota
|
||||||
|
|
||||||
|
// TemplateTypeBasicKanban is a project template type that has basic predefined columns
|
||||||
|
TemplateTypeBasicKanban
|
||||||
|
|
||||||
|
// TemplateTypeBugTriage is a project template type that has predefined columns suited to hunting down bugs
|
||||||
|
TemplateTypeBugTriage
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetTemplateConfigs retrieves the template configs of configurations project columns could have
|
||||||
|
func GetTemplateConfigs() []TemplateConfig {
|
||||||
|
return []TemplateConfig{
|
||||||
|
{TemplateTypeNone, "repo.projects.type.none"},
|
||||||
|
{TemplateTypeBasicKanban, "repo.projects.type.basic_kanban"},
|
||||||
|
{TemplateTypeBugTriage, "repo.projects.type.bug_triage"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTemplateTypeValid checks if the project template type is valid
|
||||||
|
func IsTemplateTypeValid(p TemplateType) bool {
|
||||||
|
switch p {
|
||||||
|
case TemplateTypeNone, TemplateTypeBasicKanban, TemplateTypeBugTriage:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ const (
|
||||||
TypeWiki // 5 Wiki
|
TypeWiki // 5 Wiki
|
||||||
TypeExternalWiki // 6 ExternalWiki
|
TypeExternalWiki // 6 ExternalWiki
|
||||||
TypeExternalTracker // 7 ExternalTracker
|
TypeExternalTracker // 7 ExternalTracker
|
||||||
TypeProjects // 8 Kanban board
|
TypeProjects // 8 Projects
|
||||||
TypePackages // 9 Packages
|
TypePackages // 9 Packages
|
||||||
TypeActions // 10 Actions
|
TypeActions // 10 Actions
|
||||||
)
|
)
|
||||||
|
|
|
@ -894,6 +894,10 @@ func GetUserByID(ctx context.Context, id int64) (*User, error) {
|
||||||
|
|
||||||
// GetUserByIDs returns the user objects by given IDs if exists.
|
// GetUserByIDs returns the user objects by given IDs if exists.
|
||||||
func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
users := make([]*User, 0, len(ids))
|
users := make([]*User, 0, len(ids))
|
||||||
err := db.GetEngine(ctx).In("id", ids).
|
err := db.GetEngine(ctx).In("id", ids).
|
||||||
Table("user").
|
Table("user").
|
||||||
|
|
|
@ -230,8 +230,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
||||||
if options.ProjectID.Has() {
|
if options.ProjectID.Has() {
|
||||||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectID.Value(), "project_id"))
|
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectID.Value(), "project_id"))
|
||||||
}
|
}
|
||||||
if options.ProjectBoardID.Has() {
|
if options.ProjectColumnID.Has() {
|
||||||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectBoardID.Value(), "project_board_id"))
|
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectColumnID.Value(), "project_board_id"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.PosterID.Has() {
|
if options.PosterID.Has() {
|
||||||
|
|
|
@ -61,7 +61,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
|
||||||
ReviewedID: convertID(options.ReviewedID),
|
ReviewedID: convertID(options.ReviewedID),
|
||||||
SubscriberID: convertID(options.SubscriberID),
|
SubscriberID: convertID(options.SubscriberID),
|
||||||
ProjectID: convertID(options.ProjectID),
|
ProjectID: convertID(options.ProjectID),
|
||||||
ProjectBoardID: convertID(options.ProjectBoardID),
|
ProjectColumnID: convertID(options.ProjectColumnID),
|
||||||
IsClosed: options.IsClosed,
|
IsClosed: options.IsClosed,
|
||||||
IsPull: options.IsPull,
|
IsPull: options.IsPull,
|
||||||
IncludedLabelNames: nil,
|
IncludedLabelNames: nil,
|
||||||
|
|
|
@ -50,7 +50,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
||||||
}
|
}
|
||||||
|
|
||||||
searchOpt.ProjectID = convertID(opts.ProjectID)
|
searchOpt.ProjectID = convertID(opts.ProjectID)
|
||||||
searchOpt.ProjectBoardID = convertID(opts.ProjectBoardID)
|
searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID)
|
||||||
searchOpt.PosterID = convertID(opts.PosterID)
|
searchOpt.PosterID = convertID(opts.PosterID)
|
||||||
searchOpt.AssigneeID = convertID(opts.AssigneeID)
|
searchOpt.AssigneeID = convertID(opts.AssigneeID)
|
||||||
searchOpt.MentionID = convertID(opts.MentionedID)
|
searchOpt.MentionID = convertID(opts.MentionedID)
|
||||||
|
|
|
@ -197,8 +197,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
||||||
if options.ProjectID.Has() {
|
if options.ProjectID.Has() {
|
||||||
query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value()))
|
query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value()))
|
||||||
}
|
}
|
||||||
if options.ProjectBoardID.Has() {
|
if options.ProjectColumnID.Has() {
|
||||||
query.Must(elastic.NewTermQuery("project_board_id", options.ProjectBoardID.Value()))
|
query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.PosterID.Has() {
|
if options.PosterID.Has() {
|
||||||
|
|
|
@ -369,13 +369,13 @@ func searchIssueInProject(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
SearchOptions{
|
SearchOptions{
|
||||||
ProjectBoardID: optional.Some(int64(1)),
|
ProjectColumnID: optional.Some(int64(1)),
|
||||||
},
|
},
|
||||||
[]int64{1},
|
[]int64{1},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
SearchOptions{
|
SearchOptions{
|
||||||
ProjectBoardID: optional.Some(int64(0)), // issue with in default board
|
ProjectColumnID: optional.Some(int64(0)), // issue with in default column
|
||||||
},
|
},
|
||||||
[]int64{2},
|
[]int64{2},
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,7 +27,7 @@ type IndexerData struct {
|
||||||
NoLabel bool `json:"no_label"` // True if LabelIDs is empty
|
NoLabel bool `json:"no_label"` // True if LabelIDs is empty
|
||||||
MilestoneID int64 `json:"milestone_id"`
|
MilestoneID int64 `json:"milestone_id"`
|
||||||
ProjectID int64 `json:"project_id"`
|
ProjectID int64 `json:"project_id"`
|
||||||
ProjectBoardID int64 `json:"project_board_id"`
|
ProjectColumnID int64 `json:"project_board_id"` // the key should be kept as project_board_id to keep compatible
|
||||||
PosterID int64 `json:"poster_id"`
|
PosterID int64 `json:"poster_id"`
|
||||||
AssigneeID int64 `json:"assignee_id"`
|
AssigneeID int64 `json:"assignee_id"`
|
||||||
MentionIDs []int64 `json:"mention_ids"`
|
MentionIDs []int64 `json:"mention_ids"`
|
||||||
|
@ -89,8 +89,8 @@ type SearchOptions struct {
|
||||||
|
|
||||||
MilestoneIDs []int64 // milestones the issues have
|
MilestoneIDs []int64 // milestones the issues have
|
||||||
|
|
||||||
ProjectID optional.Option[int64] // project the issues belong to
|
ProjectID optional.Option[int64] // project the issues belong to
|
||||||
ProjectBoardID optional.Option[int64] // project board the issues belong to
|
ProjectColumnID optional.Option[int64] // project column the issues belong to
|
||||||
|
|
||||||
PosterID optional.Option[int64] // poster of the issues
|
PosterID optional.Option[int64] // poster of the issues
|
||||||
|
|
||||||
|
|
|
@ -352,38 +352,38 @@ var cases = []*testIndexerCase{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "ProjectBoardID",
|
Name: "ProjectColumnID",
|
||||||
SearchOptions: &internal.SearchOptions{
|
SearchOptions: &internal.SearchOptions{
|
||||||
Paginator: &db.ListOptions{
|
Paginator: &db.ListOptions{
|
||||||
PageSize: 5,
|
PageSize: 5,
|
||||||
},
|
},
|
||||||
ProjectBoardID: optional.Some(int64(1)),
|
ProjectColumnID: optional.Some(int64(1)),
|
||||||
},
|
},
|
||||||
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
||||||
assert.Equal(t, 5, len(result.Hits))
|
assert.Equal(t, 5, len(result.Hits))
|
||||||
for _, v := range result.Hits {
|
for _, v := range result.Hits {
|
||||||
assert.Equal(t, int64(1), data[v.ID].ProjectBoardID)
|
assert.Equal(t, int64(1), data[v.ID].ProjectColumnID)
|
||||||
}
|
}
|
||||||
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
|
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
|
||||||
return v.ProjectBoardID == 1
|
return v.ProjectColumnID == 1
|
||||||
}), result.Total)
|
}), result.Total)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "no ProjectBoardID",
|
Name: "no ProjectColumnID",
|
||||||
SearchOptions: &internal.SearchOptions{
|
SearchOptions: &internal.SearchOptions{
|
||||||
Paginator: &db.ListOptions{
|
Paginator: &db.ListOptions{
|
||||||
PageSize: 5,
|
PageSize: 5,
|
||||||
},
|
},
|
||||||
ProjectBoardID: optional.Some(int64(0)),
|
ProjectColumnID: optional.Some(int64(0)),
|
||||||
},
|
},
|
||||||
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
|
||||||
assert.Equal(t, 5, len(result.Hits))
|
assert.Equal(t, 5, len(result.Hits))
|
||||||
for _, v := range result.Hits {
|
for _, v := range result.Hits {
|
||||||
assert.Equal(t, int64(0), data[v.ID].ProjectBoardID)
|
assert.Equal(t, int64(0), data[v.ID].ProjectColumnID)
|
||||||
}
|
}
|
||||||
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
|
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
|
||||||
return v.ProjectBoardID == 0
|
return v.ProjectColumnID == 0
|
||||||
}), result.Total)
|
}), result.Total)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -720,7 +720,7 @@ func generateDefaultIndexerData() []*internal.IndexerData {
|
||||||
NoLabel: len(labelIDs) == 0,
|
NoLabel: len(labelIDs) == 0,
|
||||||
MilestoneID: issueIndex % 4,
|
MilestoneID: issueIndex % 4,
|
||||||
ProjectID: issueIndex % 5,
|
ProjectID: issueIndex % 5,
|
||||||
ProjectBoardID: issueIndex % 6,
|
ProjectColumnID: issueIndex % 6,
|
||||||
PosterID: id%10 + 1, // PosterID should not be 0
|
PosterID: id%10 + 1, // PosterID should not be 0
|
||||||
AssigneeID: issueIndex % 10,
|
AssigneeID: issueIndex % 10,
|
||||||
MentionIDs: mentionIDs,
|
MentionIDs: mentionIDs,
|
||||||
|
|
|
@ -174,8 +174,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
||||||
if options.ProjectID.Has() {
|
if options.ProjectID.Has() {
|
||||||
query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value()))
|
query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value()))
|
||||||
}
|
}
|
||||||
if options.ProjectBoardID.Has() {
|
if options.ProjectColumnID.Has() {
|
||||||
query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectBoardID.Value()))
|
query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value()))
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.PosterID.Has() {
|
if options.PosterID.Has() {
|
||||||
|
|
|
@ -105,7 +105,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
|
||||||
NoLabel: len(labels) == 0,
|
NoLabel: len(labels) == 0,
|
||||||
MilestoneID: issue.MilestoneID,
|
MilestoneID: issue.MilestoneID,
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
ProjectBoardID: issue.ProjectBoardID(ctx),
|
ProjectColumnID: issue.ProjectColumnID(ctx),
|
||||||
PosterID: issue.PosterID,
|
PosterID: issue.PosterID,
|
||||||
AssigneeID: issue.AssigneeID,
|
AssigneeID: issue.AssigneeID,
|
||||||
MentionIDs: mentionIDs,
|
MentionIDs: mentionIDs,
|
||||||
|
|
|
@ -36,7 +36,7 @@ type Collector struct {
|
||||||
Oauths *prometheus.Desc
|
Oauths *prometheus.Desc
|
||||||
Organizations *prometheus.Desc
|
Organizations *prometheus.Desc
|
||||||
Projects *prometheus.Desc
|
Projects *prometheus.Desc
|
||||||
ProjectBoards *prometheus.Desc
|
ProjectColumns *prometheus.Desc
|
||||||
PublicKeys *prometheus.Desc
|
PublicKeys *prometheus.Desc
|
||||||
Releases *prometheus.Desc
|
Releases *prometheus.Desc
|
||||||
Repositories *prometheus.Desc
|
Repositories *prometheus.Desc
|
||||||
|
@ -146,9 +146,9 @@ func NewCollector() Collector {
|
||||||
"Number of projects",
|
"Number of projects",
|
||||||
nil, nil,
|
nil, nil,
|
||||||
),
|
),
|
||||||
ProjectBoards: prometheus.NewDesc(
|
ProjectColumns: prometheus.NewDesc(
|
||||||
namespace+"projects_boards",
|
namespace+"projects_boards", // TODO: change the key name will affect the consume's result history
|
||||||
"Number of project boards",
|
"Number of project columns",
|
||||||
nil, nil,
|
nil, nil,
|
||||||
),
|
),
|
||||||
PublicKeys: prometheus.NewDesc(
|
PublicKeys: prometheus.NewDesc(
|
||||||
|
@ -219,7 +219,7 @@ func (c Collector) Describe(ch chan<- *prometheus.Desc) {
|
||||||
ch <- c.Oauths
|
ch <- c.Oauths
|
||||||
ch <- c.Organizations
|
ch <- c.Organizations
|
||||||
ch <- c.Projects
|
ch <- c.Projects
|
||||||
ch <- c.ProjectBoards
|
ch <- c.ProjectColumns
|
||||||
ch <- c.PublicKeys
|
ch <- c.PublicKeys
|
||||||
ch <- c.Releases
|
ch <- c.Releases
|
||||||
ch <- c.Repositories
|
ch <- c.Repositories
|
||||||
|
@ -336,9 +336,9 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) {
|
||||||
float64(stats.Counter.Project),
|
float64(stats.Counter.Project),
|
||||||
)
|
)
|
||||||
ch <- prometheus.MustNewConstMetric(
|
ch <- prometheus.MustNewConstMetric(
|
||||||
c.ProjectBoards,
|
c.ProjectColumns,
|
||||||
prometheus.GaugeValue,
|
prometheus.GaugeValue,
|
||||||
float64(stats.Counter.ProjectBoard),
|
float64(stats.Counter.ProjectColumn),
|
||||||
)
|
)
|
||||||
ch <- prometheus.MustNewConstMetric(
|
ch <- prometheus.MustNewConstMetric(
|
||||||
c.PublicKeys,
|
c.PublicKeys,
|
||||||
|
|
|
@ -97,7 +97,7 @@ func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage,
|
||||||
log.Info("Creating Minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath)
|
log.Info("Creating Minio storage at %s:%s with base path %s", config.Endpoint, config.Bucket, config.BasePath)
|
||||||
|
|
||||||
minioClient, err := minio.New(config.Endpoint, &minio.Options{
|
minioClient, err := minio.New(config.Endpoint, &minio.Options{
|
||||||
Creds: credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, ""),
|
Creds: buildMinioCredentials(config, credentials.DefaultIAMRoleEndpoint),
|
||||||
Secure: config.UseSSL,
|
Secure: config.UseSSL,
|
||||||
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}},
|
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}},
|
||||||
Region: config.Location,
|
Region: config.Location,
|
||||||
|
@ -164,6 +164,35 @@ func (m *MinioStorage) buildMinioDirPrefix(p string) string {
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildMinioCredentials(config setting.MinioStorageConfig, iamEndpoint string) *credentials.Credentials {
|
||||||
|
// If static credentials are provided, use those
|
||||||
|
if config.AccessKeyID != "" {
|
||||||
|
return credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, fallback to a credentials chain for S3 access
|
||||||
|
chain := []credentials.Provider{
|
||||||
|
// configure based upon MINIO_ prefixed environment variables
|
||||||
|
&credentials.EnvMinio{},
|
||||||
|
// configure based upon AWS_ prefixed environment variables
|
||||||
|
&credentials.EnvAWS{},
|
||||||
|
// read credentials from MINIO_SHARED_CREDENTIALS_FILE
|
||||||
|
// environment variable, or default json config files
|
||||||
|
&credentials.FileMinioClient{},
|
||||||
|
// read credentials from AWS_SHARED_CREDENTIALS_FILE
|
||||||
|
// environment variable, or default credentials file
|
||||||
|
&credentials.FileAWSCredentials{},
|
||||||
|
// read IAM role from EC2 metadata endpoint if available
|
||||||
|
&credentials.IAM{
|
||||||
|
Endpoint: iamEndpoint,
|
||||||
|
Client: &http.Client{
|
||||||
|
Transport: http.DefaultTransport,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return credentials.NewChainCredentials(chain)
|
||||||
|
}
|
||||||
|
|
||||||
// Open opens a file
|
// Open opens a file
|
||||||
func (m *MinioStorage) Open(path string) (Object, error) {
|
func (m *MinioStorage) Open(path string) (Object, error) {
|
||||||
opts := minio.GetObjectOptions{}
|
opts := minio.GetObjectOptions{}
|
||||||
|
|
|
@ -6,6 +6,7 @@ package storage
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -109,3 +110,106 @@ func TestS3StorageBadRequest(t *testing.T) {
|
||||||
_, err := NewStorage(setting.MinioStorageType, cfg)
|
_, err := NewStorage(setting.MinioStorageType, cfg)
|
||||||
assert.ErrorContains(t, err, message)
|
assert.ErrorContains(t, err, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMinioCredentials(t *testing.T) {
|
||||||
|
const (
|
||||||
|
ExpectedAccessKey = "ExampleAccessKeyID"
|
||||||
|
ExpectedSecretAccessKey = "ExampleSecretAccessKeyID"
|
||||||
|
// Use a FakeEndpoint for IAM credentials to avoid logging any
|
||||||
|
// potential real IAM credentials when running in EC2.
|
||||||
|
FakeEndpoint = "http://localhost"
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Static Credentials", func(t *testing.T) {
|
||||||
|
cfg := setting.MinioStorageConfig{
|
||||||
|
AccessKeyID: ExpectedAccessKey,
|
||||||
|
SecretAccessKey: ExpectedSecretAccessKey,
|
||||||
|
}
|
||||||
|
creds := buildMinioCredentials(cfg, FakeEndpoint)
|
||||||
|
v, err := creds.Get()
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ExpectedAccessKey, v.AccessKeyID)
|
||||||
|
assert.Equal(t, ExpectedSecretAccessKey, v.SecretAccessKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Chain", func(t *testing.T) {
|
||||||
|
cfg := setting.MinioStorageConfig{}
|
||||||
|
|
||||||
|
t.Run("EnvMinio", func(t *testing.T) {
|
||||||
|
t.Setenv("MINIO_ACCESS_KEY", ExpectedAccessKey+"Minio")
|
||||||
|
t.Setenv("MINIO_SECRET_KEY", ExpectedSecretAccessKey+"Minio")
|
||||||
|
|
||||||
|
creds := buildMinioCredentials(cfg, FakeEndpoint)
|
||||||
|
v, err := creds.Get()
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ExpectedAccessKey+"Minio", v.AccessKeyID)
|
||||||
|
assert.Equal(t, ExpectedSecretAccessKey+"Minio", v.SecretAccessKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("EnvAWS", func(t *testing.T) {
|
||||||
|
t.Setenv("AWS_ACCESS_KEY", ExpectedAccessKey+"AWS")
|
||||||
|
t.Setenv("AWS_SECRET_KEY", ExpectedSecretAccessKey+"AWS")
|
||||||
|
|
||||||
|
creds := buildMinioCredentials(cfg, FakeEndpoint)
|
||||||
|
v, err := creds.Get()
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ExpectedAccessKey+"AWS", v.AccessKeyID)
|
||||||
|
assert.Equal(t, ExpectedSecretAccessKey+"AWS", v.SecretAccessKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FileMinio", func(t *testing.T) {
|
||||||
|
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/minio.json")
|
||||||
|
// prevent loading any actual credentials files from the user
|
||||||
|
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/fake")
|
||||||
|
|
||||||
|
creds := buildMinioCredentials(cfg, FakeEndpoint)
|
||||||
|
v, err := creds.Get()
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ExpectedAccessKey+"MinioFile", v.AccessKeyID)
|
||||||
|
assert.Equal(t, ExpectedSecretAccessKey+"MinioFile", v.SecretAccessKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FileAWS", func(t *testing.T) {
|
||||||
|
// prevent loading any actual credentials files from the user
|
||||||
|
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/fake.json")
|
||||||
|
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/aws_credentials")
|
||||||
|
|
||||||
|
creds := buildMinioCredentials(cfg, FakeEndpoint)
|
||||||
|
v, err := creds.Get()
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ExpectedAccessKey+"AWSFile", v.AccessKeyID)
|
||||||
|
assert.Equal(t, ExpectedSecretAccessKey+"AWSFile", v.SecretAccessKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("IAM", func(t *testing.T) {
|
||||||
|
// prevent loading any actual credentials files from the user
|
||||||
|
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/fake.json")
|
||||||
|
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/fake")
|
||||||
|
|
||||||
|
// Spawn a server to emulate the EC2 Instance Metadata
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// The client will actually make 3 requests here,
|
||||||
|
// first will be to get the IMDSv2 token, second to
|
||||||
|
// get the role, and third for the actual
|
||||||
|
// credentials. However, we can return credentials
|
||||||
|
// every request since we're not emulating a full
|
||||||
|
// IMDSv2 flow.
|
||||||
|
w.Write([]byte(`{"Code":"Success","AccessKeyId":"ExampleAccessKeyIDIAM","SecretAccessKey":"ExampleSecretAccessKeyIDIAM"}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Use the provided EC2 Instance Metadata server
|
||||||
|
creds := buildMinioCredentials(cfg, server.URL)
|
||||||
|
v, err := creds.Get()
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ExpectedAccessKey+"IAM", v.AccessKeyID)
|
||||||
|
assert.Equal(t, ExpectedSecretAccessKey+"IAM", v.SecretAccessKey)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
3
modules/storage/testdata/aws_credentials
vendored
Normal file
3
modules/storage/testdata/aws_credentials
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[default]
|
||||||
|
aws_access_key_id=ExampleAccessKeyIDAWSFile
|
||||||
|
aws_secret_access_key=ExampleSecretAccessKeyIDAWSFile
|
12
modules/storage/testdata/minio.json
vendored
Normal file
12
modules/storage/testdata/minio.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"version": "10",
|
||||||
|
"aliases": {
|
||||||
|
"s3": {
|
||||||
|
"url": "https://s3.amazonaws.com",
|
||||||
|
"accessKey": "ExampleAccessKeyIDMinioFile",
|
||||||
|
"secretKey": "ExampleSecretAccessKeyIDMinioFile",
|
||||||
|
"api": "S3v4",
|
||||||
|
"path": "dns"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -114,6 +114,7 @@ type Repository struct {
|
||||||
// swagger:strfmt date-time
|
// swagger:strfmt date-time
|
||||||
MirrorUpdated time.Time `json:"mirror_updated,omitempty"`
|
MirrorUpdated time.Time `json:"mirror_updated,omitempty"`
|
||||||
RepoTransfer *RepoTransfer `json:"repo_transfer"`
|
RepoTransfer *RepoTransfer `json:"repo_transfer"`
|
||||||
|
Topics []string `json:"topics"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetName implements the gitrepo.Repository interface
|
// GetName implements the gitrepo.Repository interface
|
||||||
|
|
2
options/license/Gutmann
Normal file
2
options/license/Gutmann
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
You can use this code in whatever way you want, as long as you don't try
|
||||||
|
to claim you wrote it.
|
21
options/license/HPND-export2-US
Normal file
21
options/license/HPND-export2-US
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
Copyright 2004-2008 Apple Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
Export of this software from the United States of America may
|
||||||
|
require a specific license from the United States Government.
|
||||||
|
It is the responsibility of any person or organization
|
||||||
|
contemplating export to obtain such a license before exporting.
|
||||||
|
|
||||||
|
WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
|
||||||
|
distribute this software and its documentation for any purpose and
|
||||||
|
without fee is hereby granted, provided that the above copyright
|
||||||
|
notice appear in all copies and that both that copyright notice and
|
||||||
|
this permission notice appear in supporting documentation, and that
|
||||||
|
the name of Apple Inc. not be used in advertising or publicity
|
||||||
|
pertaining to distribution of the software without specific,
|
||||||
|
written prior permission. Apple Inc. makes no representations
|
||||||
|
about the suitability of this software for any purpose. It is
|
||||||
|
provided "as is" without express or implied warranty.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
|
||||||
|
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
9
options/license/HPND-merchantability-variant
Normal file
9
options/license/HPND-merchantability-variant
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
Copyright (C) 2004 Christian Groessler <chris@groessler.org>
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and distribute this file
|
||||||
|
for any purpose is hereby granted without fee, provided that
|
||||||
|
the above copyright notice and this notice appears in all
|
||||||
|
copies.
|
||||||
|
|
||||||
|
This file is distributed WITHOUT ANY WARRANTY; without even the implied
|
||||||
|
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
66
options/license/RRDtool-FLOSS-exception-2.0
Normal file
66
options/license/RRDtool-FLOSS-exception-2.0
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
FLOSS License Exception
|
||||||
|
=======================
|
||||||
|
(Adapted from http://www.mysql.com/company/legal/licensing/foss-exception.html)
|
||||||
|
|
||||||
|
I want specified Free/Libre and Open Source Software ("FLOSS")
|
||||||
|
applications to be able to use specified GPL-licensed RRDtool
|
||||||
|
libraries (the "Program") despite the fact that not all FLOSS licenses are
|
||||||
|
compatible with version 2 of the GNU General Public License (the "GPL").
|
||||||
|
|
||||||
|
As a special exception to the terms and conditions of version 2.0 of the GPL:
|
||||||
|
|
||||||
|
You are free to distribute a Derivative Work that is formed entirely from
|
||||||
|
the Program and one or more works (each, a "FLOSS Work") licensed under one
|
||||||
|
or more of the licenses listed below, as long as:
|
||||||
|
|
||||||
|
1. You obey the GPL in all respects for the Program and the Derivative
|
||||||
|
Work, except for identifiable sections of the Derivative Work which are
|
||||||
|
not derived from the Program, and which can reasonably be considered
|
||||||
|
independent and separate works in themselves,
|
||||||
|
|
||||||
|
2. all identifiable sections of the Derivative Work which are not derived
|
||||||
|
from the Program, and which can reasonably be considered independent and
|
||||||
|
separate works in themselves,
|
||||||
|
|
||||||
|
1. are distributed subject to one of the FLOSS licenses listed
|
||||||
|
below, and
|
||||||
|
|
||||||
|
2. the object code or executable form of those sections are
|
||||||
|
accompanied by the complete corresponding machine-readable source
|
||||||
|
code for those sections on the same medium and under the same FLOSS
|
||||||
|
license as the corresponding object code or executable forms of
|
||||||
|
those sections, and
|
||||||
|
|
||||||
|
3. any works which are aggregated with the Program or with a Derivative
|
||||||
|
Work on a volume of a storage or distribution medium in accordance with
|
||||||
|
the GPL, can reasonably be considered independent and separate works in
|
||||||
|
themselves which are not derivatives of either the Program, a Derivative
|
||||||
|
Work or a FLOSS Work.
|
||||||
|
|
||||||
|
If the above conditions are not met, then the Program may only be copied,
|
||||||
|
modified, distributed or used under the terms and conditions of the GPL.
|
||||||
|
|
||||||
|
FLOSS License List
|
||||||
|
==================
|
||||||
|
License name Version(s)/Copyright Date
|
||||||
|
Academic Free License 2.0
|
||||||
|
Apache Software License 1.0/1.1/2.0
|
||||||
|
Apple Public Source License 2.0
|
||||||
|
Artistic license From Perl 5.8.0
|
||||||
|
BSD license "July 22 1999"
|
||||||
|
Common Public License 1.0
|
||||||
|
GNU Library or "Lesser" General Public License (LGPL) 2.0/2.1
|
||||||
|
IBM Public License, Version 1.0
|
||||||
|
Jabber Open Source License 1.0
|
||||||
|
MIT License (As listed in file MIT-License.txt) -
|
||||||
|
Mozilla Public License (MPL) 1.0/1.1
|
||||||
|
Open Software License 2.0
|
||||||
|
OpenSSL license (with original SSLeay license) "2003" ("1998")
|
||||||
|
PHP License 3.01
|
||||||
|
Python license (CNRI Python License) -
|
||||||
|
Python Software Foundation License 2.1.1
|
||||||
|
Sleepycat License "1999"
|
||||||
|
W3C License "2001"
|
||||||
|
X11 License "2001"
|
||||||
|
Zlib/libpng License -
|
||||||
|
Zope Public License 2.0/2.1
|
|
@ -1238,7 +1238,7 @@ tag = Tag
|
||||||
tags = Tags
|
tags = Tags
|
||||||
issues = Issues
|
issues = Issues
|
||||||
pulls = Pull requests
|
pulls = Pull requests
|
||||||
project_board = Projects
|
project = Projects
|
||||||
packages = Packages
|
packages = Packages
|
||||||
actions = Actions
|
actions = Actions
|
||||||
release = Release
|
release = Release
|
||||||
|
@ -1475,6 +1475,7 @@ issues.new.assignees = Assignees
|
||||||
issues.new.clear_assignees = Clear assignees
|
issues.new.clear_assignees = Clear assignees
|
||||||
issues.new.no_assignees = No assignees
|
issues.new.no_assignees = No assignees
|
||||||
issues.new.no_reviewers = No reviewers
|
issues.new.no_reviewers = No reviewers
|
||||||
|
issues.edit.already_changed = Unable to save changes to the issue. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes
|
||||||
issues.choose.get_started = Get started
|
issues.choose.get_started = Get started
|
||||||
issues.choose.open_external_link = Open
|
issues.choose.open_external_link = Open
|
||||||
issues.choose.blank = Default
|
issues.choose.blank = Default
|
||||||
|
@ -1792,6 +1793,7 @@ compare.compare_head = compare
|
||||||
pulls.desc = Enable pull requests and code reviews.
|
pulls.desc = Enable pull requests and code reviews.
|
||||||
pulls.new = New pull request
|
pulls.new = New pull request
|
||||||
pulls.view = View pull request
|
pulls.view = View pull request
|
||||||
|
pulls.edit.already_changed = Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes
|
||||||
pulls.compare_changes = New pull request
|
pulls.compare_changes = New pull request
|
||||||
pulls.allow_edits_from_maintainers = Allow edits from maintainers
|
pulls.allow_edits_from_maintainers = Allow edits from maintainers
|
||||||
pulls.allow_edits_from_maintainers_desc = Users with write access to the base branch can also push to this branch
|
pulls.allow_edits_from_maintainers_desc = Users with write access to the base branch can also push to this branch
|
||||||
|
@ -1947,6 +1949,8 @@ pulls.recently_pushed_new_branches = You pushed on branch <a href="%[3]s"><stron
|
||||||
|
|
||||||
pull.deleted_branch = (deleted):%s
|
pull.deleted_branch = (deleted):%s
|
||||||
|
|
||||||
|
comments.edit.already_changed = Unable to save changes to the comment. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes
|
||||||
|
|
||||||
milestones.new = New milestone
|
milestones.new = New milestone
|
||||||
milestones.closed = Closed %s
|
milestones.closed = Closed %s
|
||||||
milestones.update_ago = Updated %s
|
milestones.update_ago = Updated %s
|
||||||
|
@ -3725,6 +3729,7 @@ runs.workflow = Workflow
|
||||||
runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s
|
runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s
|
||||||
runs.no_matching_online_runner_helper = No matching online runner with label: %s
|
runs.no_matching_online_runner_helper = No matching online runner with label: %s
|
||||||
runs.no_job_without_needs = The workflow must contain at least one job without dependencies.
|
runs.no_job_without_needs = The workflow must contain at least one job without dependencies.
|
||||||
|
runs.no_job = The workflow must contain at least one job
|
||||||
runs.actor = Actor
|
runs.actor = Actor
|
||||||
runs.status = Status
|
runs.status = Status
|
||||||
runs.actors_no_select = All actors
|
runs.actors_no_select = All actors
|
||||||
|
|
1
release-notes/7.0.4/fix/4004.md
Normal file
1
release-notes/7.0.4/fix/4004.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
- "Git hooks of this repository seem to be broken." [warning when pushing more than one branch at a time](https://codeberg.org/forgejo/forgejo/commit/62448bfb931882859388b2fd472cb89428c25323)
|
4
release-notes/8.0.0/feat/3989.md
Normal file
4
release-notes/8.0.0/feat/3989.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
- API endpoints that return a repository now [also include the topics](https://codeberg.org/forgejo/forgejo/commit/ee2247d77c0b13b0b45df704d7589b541db03899)
|
||||||
|
- Display an error when an issue comment is [edited simultaneously by two users](https://codeberg.org/forgejo/forgejo/commit/ca0921a95aa9a37d8820538458c15fd0a3b0c97c) instead of silently overriding one of them
|
||||||
|
- Add [support for a credentials chain for minio](https://codeberg.org/forgejo/forgejo/commit/73706ae26d138684ef9da9e1164846a040fd4a7d)
|
||||||
|
- [Rename project board into column](https://codeberg.org/forgejo/forgejo/commit/a7591f9738dbefb2dcddeb2d45175abee3d03c1f) because it was confusing to users
|
1
release-notes/8.0.0/perf/3989.md
Normal file
1
release-notes/8.0.0/perf/3989.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
- improve performances when [retrieving pull requests via the API](https://codeberg.org/forgejo/forgejo/commit/47a2102694c47bc30a2a7c673c328471839ef206)
|
|
@ -816,8 +816,13 @@ func EditIssue(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if form.Body != nil {
|
if form.Body != nil {
|
||||||
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body)
|
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, issue.ContentVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
|
||||||
|
ctx.Error(http.StatusBadRequest, "ChangeContent", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
|
ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -220,7 +220,7 @@ func CreateIssueAttachment(ctx *context.APIContext) {
|
||||||
|
|
||||||
issue.Attachments = append(issue.Attachments, attachment)
|
issue.Attachments = append(issue.Attachments, attachment)
|
||||||
|
|
||||||
if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, issue.Content); err != nil {
|
if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, issue.Content, issue.ContentVersion); err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
|
ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -586,7 +586,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
|
||||||
|
|
||||||
oldContent := comment.Content
|
oldContent := comment.Content
|
||||||
comment.Content = form.Body
|
comment.Content = form.Body
|
||||||
if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
|
if err := issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, oldContent); err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
|
ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -225,7 +225,7 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil {
|
if err = issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, comment.Content); err != nil {
|
||||||
ctx.ServerError("UpdateComment", err)
|
ctx.ServerError("UpdateComment", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,23 +116,39 @@ func ListPullRequests(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
apiPrs := make([]*api.PullRequest, len(prs))
|
apiPrs := make([]*api.PullRequest, len(prs))
|
||||||
|
// NOTE: load repository first, so that issue.Repo will be filled with pr.BaseRepo
|
||||||
|
if err := prs.LoadRepositories(ctx); err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "LoadRepositories", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
issueList, err := prs.LoadIssues(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "LoadIssues", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := issueList.LoadLabels(ctx); err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "LoadLabels", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := issueList.LoadPosters(ctx); err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "LoadPoster", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := issueList.LoadAttachments(ctx); err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := issueList.LoadMilestones(ctx); err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "LoadMilestones", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := issueList.LoadAssignees(ctx); err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "LoadAssignees", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for i := range prs {
|
for i := range prs {
|
||||||
if err = prs[i].LoadIssue(ctx); err != nil {
|
|
||||||
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = prs[i].LoadAttributes(ctx); err != nil {
|
|
||||||
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = prs[i].LoadBaseRepo(ctx); err != nil {
|
|
||||||
ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = prs[i].LoadHeadRepo(ctx); err != nil {
|
|
||||||
ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
apiPrs[i] = convert.ToAPIPullRequest(ctx, prs[i], ctx.Doer)
|
apiPrs[i] = convert.ToAPIPullRequest(ctx, prs[i], ctx.Doer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -609,8 +625,13 @@ func EditPullRequest(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if form.Body != nil {
|
if form.Body != nil {
|
||||||
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body)
|
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, issue.ContentVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
|
||||||
|
ctx.Error(http.StatusBadRequest, "ChangeContent", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
|
ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,6 +94,7 @@ func UnadoptedRepos(ctx *context.Context) {
|
||||||
repoNames, count, err := repo_service.ListUnadoptedRepositories(ctx, q, &opts)
|
repoNames, count, err := repo_service.ListUnadoptedRepositories(ctx, q, &opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("ListUnadoptedRepositories", err)
|
ctx.ServerError("ListUnadoptedRepositories", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Dirs"] = repoNames
|
ctx.Data["Dirs"] = repoNames
|
||||||
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
|
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
|
||||||
|
|
|
@ -840,6 +840,7 @@ func ActivateEmail(ctx *context.Context) {
|
||||||
if email := user_model.VerifyActiveEmailCode(ctx, code, emailStr); email != nil {
|
if email := user_model.VerifyActiveEmailCode(ctx, code, emailStr); email != nil {
|
||||||
if err := user_model.ActivateEmail(ctx, email); err != nil {
|
if err := user_model.ActivateEmail(ctx, email); err != nil {
|
||||||
ctx.ServerError("ActivateEmail", err)
|
ctx.ServerError("ActivateEmail", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace("Email activated: %s", email.Email)
|
log.Trace("Email activated: %s", email.Email)
|
||||||
|
|
|
@ -34,7 +34,7 @@ const (
|
||||||
// MustEnableProjects check if projects are enabled in settings
|
// MustEnableProjects check if projects are enabled in settings
|
||||||
func MustEnableProjects(ctx *context.Context) {
|
func MustEnableProjects(ctx *context.Context) {
|
||||||
if unit.TypeProjects.UnitGlobalDisabled() {
|
if unit.TypeProjects.UnitGlobalDisabled() {
|
||||||
ctx.NotFound("EnableKanbanBoard", nil)
|
ctx.NotFound("EnableProjects", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ func MustEnableProjects(ctx *context.Context) {
|
||||||
// Projects renders the home page of projects
|
// Projects renders the home page of projects
|
||||||
func Projects(ctx *context.Context) {
|
func Projects(ctx *context.Context) {
|
||||||
shared_user.PrepareContextForProfileBigAvatar(ctx)
|
shared_user.PrepareContextForProfileBigAvatar(ctx)
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.project_board")
|
ctx.Data["Title"] = ctx.Tr("repo.projects")
|
||||||
|
|
||||||
sortType := ctx.FormTrim("sort")
|
sortType := ctx.FormTrim("sort")
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ func canWriteProjects(ctx *context.Context) bool {
|
||||||
// RenderNewProject render creating a project page
|
// RenderNewProject render creating a project page
|
||||||
func RenderNewProject(ctx *context.Context) {
|
func RenderNewProject(ctx *context.Context) {
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
|
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
|
||||||
ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
|
ctx.Data["TemplateConfigs"] = project_model.GetTemplateConfigs()
|
||||||
ctx.Data["CardTypes"] = project_model.GetCardConfig()
|
ctx.Data["CardTypes"] = project_model.GetCardConfig()
|
||||||
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
|
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
|
||||||
ctx.Data["PageIsViewProjects"] = true
|
ctx.Data["PageIsViewProjects"] = true
|
||||||
|
@ -168,12 +168,12 @@ func NewProjectPost(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
newProject := project_model.Project{
|
newProject := project_model.Project{
|
||||||
OwnerID: ctx.ContextUser.ID,
|
OwnerID: ctx.ContextUser.ID,
|
||||||
Title: form.Title,
|
Title: form.Title,
|
||||||
Description: form.Content,
|
Description: form.Content,
|
||||||
CreatorID: ctx.Doer.ID,
|
CreatorID: ctx.Doer.ID,
|
||||||
BoardType: form.BoardType,
|
TemplateType: form.TemplateType,
|
||||||
CardType: form.CardType,
|
CardType: form.CardType,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.ContextUser.IsOrganization() {
|
if ctx.ContextUser.IsOrganization() {
|
||||||
|
@ -314,7 +314,7 @@ func EditProjectPost(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ViewProject renders the project board for a project
|
// ViewProject renders the project with board view for a project
|
||||||
func ViewProject(ctx *context.Context) {
|
func ViewProject(ctx *context.Context) {
|
||||||
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -326,15 +326,15 @@ func ViewProject(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
boards, err := project.GetBoards(ctx)
|
columns, err := project.GetColumns(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetProjectBoards", err)
|
ctx.ServerError("GetProjectColumns", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
|
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("LoadIssuesOfBoards", err)
|
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,7 +377,7 @@ func ViewProject(ctx *context.Context) {
|
||||||
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
|
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
|
||||||
ctx.Data["Project"] = project
|
ctx.Data["Project"] = project
|
||||||
ctx.Data["IssuesMap"] = issuesMap
|
ctx.Data["IssuesMap"] = issuesMap
|
||||||
ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend
|
ctx.Data["Columns"] = columns
|
||||||
shared_user.RenderUserHeader(ctx)
|
shared_user.RenderUserHeader(ctx)
|
||||||
|
|
||||||
err = shared_user.LoadHeaderCount(ctx)
|
err = shared_user.LoadHeaderCount(ctx)
|
||||||
|
@ -389,8 +389,8 @@ func ViewProject(ctx *context.Context) {
|
||||||
ctx.HTML(http.StatusOK, tplProjectsView)
|
ctx.HTML(http.StatusOK, tplProjectsView)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteProjectBoard allows for the deletion of a project board
|
// DeleteProjectColumn allows for the deletion of a project column
|
||||||
func DeleteProjectBoard(ctx *context.Context) {
|
func DeleteProjectColumn(ctx *context.Context) {
|
||||||
if ctx.Doer == nil {
|
if ctx.Doer == nil {
|
||||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||||
"message": "Only signed in users are allowed to perform this action.",
|
"message": "Only signed in users are allowed to perform this action.",
|
||||||
|
@ -404,36 +404,36 @@ func DeleteProjectBoard(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
|
pb, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetProjectBoard", err)
|
ctx.ServerError("GetProjectColumn", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if pb.ProjectID != ctx.ParamsInt64(":id") {
|
if pb.ProjectID != ctx.ParamsInt64(":id") {
|
||||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
||||||
"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID),
|
"message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if project.OwnerID != ctx.ContextUser.ID {
|
if project.OwnerID != ctx.ContextUser.ID {
|
||||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
||||||
"message": fmt.Sprintf("ProjectBoard[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID),
|
"message": fmt.Sprintf("ProjectColumn[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := project_model.DeleteBoardByID(ctx, ctx.ParamsInt64(":boardID")); err != nil {
|
if err := project_model.DeleteColumnByID(ctx, ctx.ParamsInt64(":columnID")); err != nil {
|
||||||
ctx.ServerError("DeleteProjectBoardByID", err)
|
ctx.ServerError("DeleteProjectColumnByID", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSONOK()
|
ctx.JSONOK()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddBoardToProjectPost allows a new board to be added to a project.
|
// AddColumnToProjectPost allows a new column to be added to a project.
|
||||||
func AddBoardToProjectPost(ctx *context.Context) {
|
func AddColumnToProjectPost(ctx *context.Context) {
|
||||||
form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
|
form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
|
||||||
|
|
||||||
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -441,21 +441,21 @@ func AddBoardToProjectPost(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := project_model.NewBoard(ctx, &project_model.Board{
|
if err := project_model.NewColumn(ctx, &project_model.Column{
|
||||||
ProjectID: project.ID,
|
ProjectID: project.ID,
|
||||||
Title: form.Title,
|
Title: form.Title,
|
||||||
Color: form.Color,
|
Color: form.Color,
|
||||||
CreatorID: ctx.Doer.ID,
|
CreatorID: ctx.Doer.ID,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
ctx.ServerError("NewProjectBoard", err)
|
ctx.ServerError("NewProjectColumn", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSONOK()
|
ctx.JSONOK()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckProjectBoardChangePermissions check permission
|
// CheckProjectColumnChangePermissions check permission
|
||||||
func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) {
|
func CheckProjectColumnChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Column) {
|
||||||
if ctx.Doer == nil {
|
if ctx.Doer == nil {
|
||||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||||
"message": "Only signed in users are allowed to perform this action.",
|
"message": "Only signed in users are allowed to perform this action.",
|
||||||
|
@ -469,62 +469,60 @@ func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
|
column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetProjectBoard", err)
|
ctx.ServerError("GetProjectColumn", err)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if board.ProjectID != ctx.ParamsInt64(":id") {
|
if column.ProjectID != ctx.ParamsInt64(":id") {
|
||||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
||||||
"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID),
|
"message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID),
|
||||||
})
|
})
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if project.OwnerID != ctx.ContextUser.ID {
|
if project.OwnerID != ctx.ContextUser.ID {
|
||||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
||||||
"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, project.ID),
|
"message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", column.ID, project.ID),
|
||||||
})
|
})
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return project, board
|
return project, column
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditProjectBoard allows a project board's to be updated
|
// EditProjectColumn allows a project column's to be updated
|
||||||
func EditProjectBoard(ctx *context.Context) {
|
func EditProjectColumn(ctx *context.Context) {
|
||||||
form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
|
form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
|
||||||
_, board := CheckProjectBoardChangePermissions(ctx)
|
_, column := CheckProjectColumnChangePermissions(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Title != "" {
|
if form.Title != "" {
|
||||||
board.Title = form.Title
|
column.Title = form.Title
|
||||||
}
|
}
|
||||||
|
column.Color = form.Color
|
||||||
board.Color = form.Color
|
|
||||||
|
|
||||||
if form.Sorting != 0 {
|
if form.Sorting != 0 {
|
||||||
board.Sorting = form.Sorting
|
column.Sorting = form.Sorting
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := project_model.UpdateBoard(ctx, board); err != nil {
|
if err := project_model.UpdateColumn(ctx, column); err != nil {
|
||||||
ctx.ServerError("UpdateProjectBoard", err)
|
ctx.ServerError("UpdateProjectColumn", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSONOK()
|
ctx.JSONOK()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaultProjectBoard set default board for uncategorized issues/pulls
|
// SetDefaultProjectColumn set default column for uncategorized issues/pulls
|
||||||
func SetDefaultProjectBoard(ctx *context.Context) {
|
func SetDefaultProjectColumn(ctx *context.Context) {
|
||||||
project, board := CheckProjectBoardChangePermissions(ctx)
|
project, column := CheckProjectColumnChangePermissions(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := project_model.SetDefaultBoard(ctx, project.ID, board.ID); err != nil {
|
if err := project_model.SetDefaultColumn(ctx, project.ID, column.ID); err != nil {
|
||||||
ctx.ServerError("SetDefaultBoard", err)
|
ctx.ServerError("SetDefaultColumn", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -550,14 +548,14 @@ func MoveIssues(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
|
column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.NotFoundOrServerError("GetProjectBoard", project_model.IsErrProjectBoardNotExist, err)
|
ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if board.ProjectID != project.ID {
|
if column.ProjectID != project.ID {
|
||||||
ctx.NotFound("BoardNotInProject", nil)
|
ctx.NotFound("ColumnNotInProject", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -571,6 +569,7 @@ func MoveIssues(ctx *context.Context) {
|
||||||
form := &movedIssuesForm{}
|
form := &movedIssuesForm{}
|
||||||
if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
|
if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
|
||||||
ctx.ServerError("DecodeMovedIssuesForm", err)
|
ctx.ServerError("DecodeMovedIssuesForm", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
issueIDs := make([]int64, 0, len(form.Issues))
|
issueIDs := make([]int64, 0, len(form.Issues))
|
||||||
|
@ -602,8 +601,8 @@ func MoveIssues(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil {
|
if err = project_model.MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs); err != nil {
|
||||||
ctx.ServerError("MoveIssuesOnProjectBoard", err)
|
ctx.ServerError("MoveIssuesOnProjectColumn", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,16 +13,16 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCheckProjectBoardChangePermissions(t *testing.T) {
|
func TestCheckProjectColumnChangePermissions(t *testing.T) {
|
||||||
unittest.PrepareTestEnv(t)
|
unittest.PrepareTestEnv(t)
|
||||||
ctx, _ := contexttest.MockContext(t, "user2/-/projects/4/4")
|
ctx, _ := contexttest.MockContext(t, "user2/-/projects/4/4")
|
||||||
contexttest.LoadUser(t, ctx, 2)
|
contexttest.LoadUser(t, ctx, 2)
|
||||||
ctx.ContextUser = ctx.Doer // user2
|
ctx.ContextUser = ctx.Doer // user2
|
||||||
ctx.SetParams(":id", "4")
|
ctx.SetParams(":id", "4")
|
||||||
ctx.SetParams(":boardID", "4")
|
ctx.SetParams(":columnID", "4")
|
||||||
|
|
||||||
project, board := org.CheckProjectBoardChangePermissions(ctx)
|
project, column := org.CheckProjectColumnChangePermissions(ctx)
|
||||||
assert.NotNil(t, project)
|
assert.NotNil(t, project)
|
||||||
assert.NotNil(t, board)
|
assert.NotNil(t, column)
|
||||||
assert.False(t, ctx.Written())
|
assert.False(t, ctx.Written())
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,12 @@ func List(ctx *context.Context) {
|
||||||
// The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run.
|
// The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run.
|
||||||
hasJobWithoutNeeds := false
|
hasJobWithoutNeeds := false
|
||||||
// Check whether have matching runner and a job without "needs"
|
// Check whether have matching runner and a job without "needs"
|
||||||
|
emptyJobsNumber := 0
|
||||||
for _, j := range wf.Jobs {
|
for _, j := range wf.Jobs {
|
||||||
|
if j == nil {
|
||||||
|
emptyJobsNumber++
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !hasJobWithoutNeeds && len(j.Needs()) == 0 {
|
if !hasJobWithoutNeeds && len(j.Needs()) == 0 {
|
||||||
hasJobWithoutNeeds = true
|
hasJobWithoutNeeds = true
|
||||||
}
|
}
|
||||||
|
@ -131,6 +136,9 @@ func List(ctx *context.Context) {
|
||||||
if !hasJobWithoutNeeds {
|
if !hasJobWithoutNeeds {
|
||||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
|
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
|
||||||
}
|
}
|
||||||
|
if emptyJobsNumber == len(wf.Jobs) {
|
||||||
|
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
|
||||||
|
}
|
||||||
workflows = append(workflows, workflow)
|
workflows = append(workflows, workflow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -604,6 +604,7 @@ func DeleteFilePost(ctx *context.Context) {
|
||||||
} else {
|
} else {
|
||||||
ctx.ServerError("DeleteRepoFile", err)
|
ctx.ServerError("DeleteRepoFile", err)
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
|
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
|
||||||
|
|
|
@ -2239,8 +2239,16 @@ func UpdateIssueContent(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content")); err != nil {
|
if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content"), ctx.FormInt("content_version")); err != nil {
|
||||||
ctx.ServerError("ChangeContent", err)
|
if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
|
||||||
|
if issue.IsPull {
|
||||||
|
ctx.JSONError(ctx.Tr("repo.pulls.edit.already_changed"))
|
||||||
|
} else {
|
||||||
|
ctx.JSONError(ctx.Tr("repo.issues.edit.already_changed"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("ChangeContent", err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2266,8 +2274,9 @@ func UpdateIssueContent(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, map[string]any{
|
ctx.JSON(http.StatusOK, map[string]any{
|
||||||
"content": content,
|
"content": content,
|
||||||
"attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content),
|
"contentVersion": issue.ContentVersion,
|
||||||
|
"attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2822,12 +2831,12 @@ func ListIssues(ctx *context.Context) {
|
||||||
Page: ctx.FormInt("page"),
|
Page: ctx.FormInt("page"),
|
||||||
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
||||||
},
|
},
|
||||||
Keyword: keyword,
|
Keyword: keyword,
|
||||||
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||||
IsPull: isPull,
|
IsPull: isPull,
|
||||||
IsClosed: isClosed,
|
IsClosed: isClosed,
|
||||||
ProjectBoardID: projectID,
|
ProjectID: projectID,
|
||||||
SortBy: issue_indexer.SortByCreatedDesc,
|
SortBy: issue_indexer.SortByCreatedDesc,
|
||||||
}
|
}
|
||||||
if since != 0 {
|
if since != 0 {
|
||||||
searchOpt.UpdatedAfterUnix = optional.Some(since)
|
searchOpt.UpdatedAfterUnix = optional.Some(since)
|
||||||
|
@ -3155,9 +3164,16 @@ func UpdateCommentContent(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
oldContent := comment.Content
|
oldContent := comment.Content
|
||||||
comment.Content = ctx.FormString("content")
|
newContent := ctx.FormString("content")
|
||||||
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
|
contentVersion := ctx.FormInt("content_version")
|
||||||
ctx.ServerError("UpdateComment", err)
|
|
||||||
|
comment.Content = newContent
|
||||||
|
if err = issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
|
||||||
|
if errors.Is(err, issues_model.ErrCommentAlreadyChanged) {
|
||||||
|
ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("UpdateComment", err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3188,8 +3204,9 @@ func UpdateCommentContent(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, map[string]any{
|
ctx.JSON(http.StatusOK, map[string]any{
|
||||||
"content": content,
|
"content": content,
|
||||||
"attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
|
"contentVersion": comment.ContentVersion,
|
||||||
|
"attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ const (
|
||||||
// MustEnableProjects check if projects are enabled in settings
|
// MustEnableProjects check if projects are enabled in settings
|
||||||
func MustEnableProjects(ctx *context.Context) {
|
func MustEnableProjects(ctx *context.Context) {
|
||||||
if unit.TypeProjects.UnitGlobalDisabled() {
|
if unit.TypeProjects.UnitGlobalDisabled() {
|
||||||
ctx.NotFound("EnableKanbanBoard", nil)
|
ctx.NotFound("EnableRepoProjects", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ func MustEnableProjects(ctx *context.Context) {
|
||||||
|
|
||||||
// Projects renders the home page of projects
|
// Projects renders the home page of projects
|
||||||
func Projects(ctx *context.Context) {
|
func Projects(ctx *context.Context) {
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.project_board")
|
ctx.Data["Title"] = ctx.Tr("repo.projects")
|
||||||
|
|
||||||
sortType := ctx.FormTrim("sort")
|
sortType := ctx.FormTrim("sort")
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ func Projects(ctx *context.Context) {
|
||||||
// RenderNewProject render creating a project page
|
// RenderNewProject render creating a project page
|
||||||
func RenderNewProject(ctx *context.Context) {
|
func RenderNewProject(ctx *context.Context) {
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
|
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
|
||||||
ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
|
ctx.Data["TemplateConfigs"] = project_model.GetTemplateConfigs()
|
||||||
ctx.Data["CardTypes"] = project_model.GetCardConfig()
|
ctx.Data["CardTypes"] = project_model.GetCardConfig()
|
||||||
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
|
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
|
||||||
ctx.Data["CancelLink"] = ctx.Repo.Repository.Link() + "/projects"
|
ctx.Data["CancelLink"] = ctx.Repo.Repository.Link() + "/projects"
|
||||||
|
@ -149,13 +149,13 @@ func NewProjectPost(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := project_model.NewProject(ctx, &project_model.Project{
|
if err := project_model.NewProject(ctx, &project_model.Project{
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
Title: form.Title,
|
Title: form.Title,
|
||||||
Description: form.Content,
|
Description: form.Content,
|
||||||
CreatorID: ctx.Doer.ID,
|
CreatorID: ctx.Doer.ID,
|
||||||
BoardType: form.BoardType,
|
TemplateType: form.TemplateType,
|
||||||
CardType: form.CardType,
|
CardType: form.CardType,
|
||||||
Type: project_model.TypeRepository,
|
Type: project_model.TypeRepository,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
ctx.ServerError("NewProject", err)
|
ctx.ServerError("NewProject", err)
|
||||||
return
|
return
|
||||||
|
@ -288,7 +288,7 @@ func EditProjectPost(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ViewProject renders the project board for a project
|
// ViewProject renders the project with board view
|
||||||
func ViewProject(ctx *context.Context) {
|
func ViewProject(ctx *context.Context) {
|
||||||
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -304,15 +304,15 @@ func ViewProject(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
boards, err := project.GetBoards(ctx)
|
columns, err := project.GetColumns(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetProjectBoards", err)
|
ctx.ServerError("GetProjectColumns", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
|
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("LoadIssuesOfBoards", err)
|
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,7 +367,7 @@ func ViewProject(ctx *context.Context) {
|
||||||
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
|
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
|
||||||
ctx.Data["Project"] = project
|
ctx.Data["Project"] = project
|
||||||
ctx.Data["IssuesMap"] = issuesMap
|
ctx.Data["IssuesMap"] = issuesMap
|
||||||
ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend
|
ctx.Data["Columns"] = columns
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplProjectsView)
|
ctx.HTML(http.StatusOK, tplProjectsView)
|
||||||
}
|
}
|
||||||
|
@ -405,8 +405,8 @@ func UpdateIssueProject(ctx *context.Context) {
|
||||||
ctx.JSONOK()
|
ctx.JSONOK()
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteProjectBoard allows for the deletion of a project board
|
// DeleteProjectColumn allows for the deletion of a project column
|
||||||
func DeleteProjectBoard(ctx *context.Context) {
|
func DeleteProjectColumn(ctx *context.Context) {
|
||||||
if ctx.Doer == nil {
|
if ctx.Doer == nil {
|
||||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||||
"message": "Only signed in users are allowed to perform this action.",
|
"message": "Only signed in users are allowed to perform this action.",
|
||||||
|
@ -431,36 +431,36 @@ func DeleteProjectBoard(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
|
pb, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetProjectBoard", err)
|
ctx.ServerError("GetProjectColumn", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if pb.ProjectID != ctx.ParamsInt64(":id") {
|
if pb.ProjectID != ctx.ParamsInt64(":id") {
|
||||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
||||||
"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", pb.ID, project.ID),
|
"message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if project.RepoID != ctx.Repo.Repository.ID {
|
if project.RepoID != ctx.Repo.Repository.ID {
|
||||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
||||||
"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", pb.ID, ctx.Repo.Repository.ID),
|
"message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", pb.ID, ctx.Repo.Repository.ID),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := project_model.DeleteBoardByID(ctx, ctx.ParamsInt64(":boardID")); err != nil {
|
if err := project_model.DeleteColumnByID(ctx, ctx.ParamsInt64(":columnID")); err != nil {
|
||||||
ctx.ServerError("DeleteProjectBoardByID", err)
|
ctx.ServerError("DeleteProjectColumnByID", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSONOK()
|
ctx.JSONOK()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddBoardToProjectPost allows a new board to be added to a project.
|
// AddColumnToProjectPost allows a new column to be added to a project.
|
||||||
func AddBoardToProjectPost(ctx *context.Context) {
|
func AddColumnToProjectPost(ctx *context.Context) {
|
||||||
form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
|
form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
|
||||||
if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) {
|
if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) {
|
||||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||||
"message": "Only authorized users are allowed to perform this action.",
|
"message": "Only authorized users are allowed to perform this action.",
|
||||||
|
@ -478,20 +478,20 @@ func AddBoardToProjectPost(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := project_model.NewBoard(ctx, &project_model.Board{
|
if err := project_model.NewColumn(ctx, &project_model.Column{
|
||||||
ProjectID: project.ID,
|
ProjectID: project.ID,
|
||||||
Title: form.Title,
|
Title: form.Title,
|
||||||
Color: form.Color,
|
Color: form.Color,
|
||||||
CreatorID: ctx.Doer.ID,
|
CreatorID: ctx.Doer.ID,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
ctx.ServerError("NewProjectBoard", err)
|
ctx.ServerError("NewProjectColumn", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSONOK()
|
ctx.JSONOK()
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) {
|
func checkProjectColumnChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Column) {
|
||||||
if ctx.Doer == nil {
|
if ctx.Doer == nil {
|
||||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||||
"message": "Only signed in users are allowed to perform this action.",
|
"message": "Only signed in users are allowed to perform this action.",
|
||||||
|
@ -516,62 +516,60 @@ func checkProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
|
column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetProjectBoard", err)
|
ctx.ServerError("GetProjectColumn", err)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if board.ProjectID != ctx.ParamsInt64(":id") {
|
if column.ProjectID != ctx.ParamsInt64(":id") {
|
||||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
||||||
"message": fmt.Sprintf("ProjectBoard[%d] is not in Project[%d] as expected", board.ID, project.ID),
|
"message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID),
|
||||||
})
|
})
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if project.RepoID != ctx.Repo.Repository.ID {
|
if project.RepoID != ctx.Repo.Repository.ID {
|
||||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
|
||||||
"message": fmt.Sprintf("ProjectBoard[%d] is not in Repository[%d] as expected", board.ID, ctx.Repo.Repository.ID),
|
"message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", column.ID, ctx.Repo.Repository.ID),
|
||||||
})
|
})
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return project, board
|
return project, column
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditProjectBoard allows a project board's to be updated
|
// EditProjectColumn allows a project column's to be updated
|
||||||
func EditProjectBoard(ctx *context.Context) {
|
func EditProjectColumn(ctx *context.Context) {
|
||||||
form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
|
form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
|
||||||
_, board := checkProjectBoardChangePermissions(ctx)
|
_, column := checkProjectColumnChangePermissions(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Title != "" {
|
if form.Title != "" {
|
||||||
board.Title = form.Title
|
column.Title = form.Title
|
||||||
}
|
}
|
||||||
|
column.Color = form.Color
|
||||||
board.Color = form.Color
|
|
||||||
|
|
||||||
if form.Sorting != 0 {
|
if form.Sorting != 0 {
|
||||||
board.Sorting = form.Sorting
|
column.Sorting = form.Sorting
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := project_model.UpdateBoard(ctx, board); err != nil {
|
if err := project_model.UpdateColumn(ctx, column); err != nil {
|
||||||
ctx.ServerError("UpdateProjectBoard", err)
|
ctx.ServerError("UpdateProjectColumn", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSONOK()
|
ctx.JSONOK()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaultProjectBoard set default board for uncategorized issues/pulls
|
// SetDefaultProjectColumn set default column for uncategorized issues/pulls
|
||||||
func SetDefaultProjectBoard(ctx *context.Context) {
|
func SetDefaultProjectColumn(ctx *context.Context) {
|
||||||
project, board := checkProjectBoardChangePermissions(ctx)
|
project, column := checkProjectColumnChangePermissions(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := project_model.SetDefaultBoard(ctx, project.ID, board.ID); err != nil {
|
if err := project_model.SetDefaultColumn(ctx, project.ID, column.ID); err != nil {
|
||||||
ctx.ServerError("SetDefaultBoard", err)
|
ctx.ServerError("SetDefaultColumn", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -608,18 +606,18 @@ func MoveIssues(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
|
column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if project_model.IsErrProjectBoardNotExist(err) {
|
if project_model.IsErrProjectColumnNotExist(err) {
|
||||||
ctx.NotFound("ProjectBoardNotExist", nil)
|
ctx.NotFound("ProjectColumnNotExist", nil)
|
||||||
} else {
|
} else {
|
||||||
ctx.ServerError("GetProjectBoard", err)
|
ctx.ServerError("GetProjectColumn", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if board.ProjectID != project.ID {
|
if column.ProjectID != project.ID {
|
||||||
ctx.NotFound("BoardNotInProject", nil)
|
ctx.NotFound("ColumnNotInProject", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -663,8 +661,8 @@ func MoveIssues(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil {
|
if err = project_model.MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs); err != nil {
|
||||||
ctx.ServerError("MoveIssuesOnProjectBoard", err)
|
ctx.ServerError("MoveIssuesOnProjectColumn", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,16 +12,16 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCheckProjectBoardChangePermissions(t *testing.T) {
|
func TestCheckProjectColumnChangePermissions(t *testing.T) {
|
||||||
unittest.PrepareTestEnv(t)
|
unittest.PrepareTestEnv(t)
|
||||||
ctx, _ := contexttest.MockContext(t, "user2/repo1/projects/1/2")
|
ctx, _ := contexttest.MockContext(t, "user2/repo1/projects/1/2")
|
||||||
contexttest.LoadUser(t, ctx, 2)
|
contexttest.LoadUser(t, ctx, 2)
|
||||||
contexttest.LoadRepo(t, ctx, 1)
|
contexttest.LoadRepo(t, ctx, 1)
|
||||||
ctx.SetParams(":id", "1")
|
ctx.SetParams(":id", "1")
|
||||||
ctx.SetParams(":boardID", "2")
|
ctx.SetParams(":columnID", "2")
|
||||||
|
|
||||||
project, board := checkProjectBoardChangePermissions(ctx)
|
project, column := checkProjectColumnChangePermissions(ctx)
|
||||||
assert.NotNil(t, project)
|
assert.NotNil(t, project)
|
||||||
assert.NotNil(t, board)
|
assert.NotNil(t, column)
|
||||||
assert.False(t, ctx.Written())
|
assert.False(t, ctx.Written())
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,9 +79,8 @@ func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: 30,
|
PageSize: 30,
|
||||||
},
|
},
|
||||||
Status: actions_model.StatusUnknown, // Unknown means all
|
Status: actions_model.StatusUnknown, // Unknown means all
|
||||||
IDOrderDesc: true,
|
RunnerID: runner.ID,
|
||||||
RunnerID: runner.ID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks, count, err := db.FindAndCount[actions_model.ActionTask](ctx, opts)
|
tasks, count, err := db.FindAndCount[actions_model.ActionTask](ctx, opts)
|
||||||
|
|
|
@ -978,7 +978,7 @@ func registerRoutes(m *web.Route) {
|
||||||
m.Get("/new", org.RenderNewProject)
|
m.Get("/new", org.RenderNewProject)
|
||||||
m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
|
m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
|
||||||
m.Group("/{id}", func() {
|
m.Group("/{id}", func() {
|
||||||
m.Post("", web.Bind(forms.EditProjectBoardForm{}), org.AddBoardToProjectPost)
|
m.Post("", web.Bind(forms.EditProjectColumnForm{}), org.AddColumnToProjectPost)
|
||||||
m.Post("/move", project.MoveColumns)
|
m.Post("/move", project.MoveColumns)
|
||||||
m.Post("/delete", org.DeleteProject)
|
m.Post("/delete", org.DeleteProject)
|
||||||
|
|
||||||
|
@ -986,10 +986,10 @@ func registerRoutes(m *web.Route) {
|
||||||
m.Post("/edit", web.Bind(forms.CreateProjectForm{}), org.EditProjectPost)
|
m.Post("/edit", web.Bind(forms.CreateProjectForm{}), org.EditProjectPost)
|
||||||
m.Post("/{action:open|close}", org.ChangeProjectStatus)
|
m.Post("/{action:open|close}", org.ChangeProjectStatus)
|
||||||
|
|
||||||
m.Group("/{boardID}", func() {
|
m.Group("/{columnID}", func() {
|
||||||
m.Put("", web.Bind(forms.EditProjectBoardForm{}), org.EditProjectBoard)
|
m.Put("", web.Bind(forms.EditProjectColumnForm{}), org.EditProjectColumn)
|
||||||
m.Delete("", org.DeleteProjectBoard)
|
m.Delete("", org.DeleteProjectColumn)
|
||||||
m.Post("/default", org.SetDefaultProjectBoard)
|
m.Post("/default", org.SetDefaultProjectColumn)
|
||||||
|
|
||||||
m.Post("/move", org.MoveIssues)
|
m.Post("/move", org.MoveIssues)
|
||||||
})
|
})
|
||||||
|
@ -1352,7 +1352,7 @@ func registerRoutes(m *web.Route) {
|
||||||
m.Get("/new", repo.RenderNewProject)
|
m.Get("/new", repo.RenderNewProject)
|
||||||
m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
|
m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
|
||||||
m.Group("/{id}", func() {
|
m.Group("/{id}", func() {
|
||||||
m.Post("", web.Bind(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost)
|
m.Post("", web.Bind(forms.EditProjectColumnForm{}), repo.AddColumnToProjectPost)
|
||||||
m.Post("/move", project.MoveColumns)
|
m.Post("/move", project.MoveColumns)
|
||||||
m.Post("/delete", repo.DeleteProject)
|
m.Post("/delete", repo.DeleteProject)
|
||||||
|
|
||||||
|
@ -1360,10 +1360,10 @@ func registerRoutes(m *web.Route) {
|
||||||
m.Post("/edit", web.Bind(forms.CreateProjectForm{}), repo.EditProjectPost)
|
m.Post("/edit", web.Bind(forms.CreateProjectForm{}), repo.EditProjectPost)
|
||||||
m.Post("/{action:open|close}", repo.ChangeProjectStatus)
|
m.Post("/{action:open|close}", repo.ChangeProjectStatus)
|
||||||
|
|
||||||
m.Group("/{boardID}", func() {
|
m.Group("/{columnID}", func() {
|
||||||
m.Put("", web.Bind(forms.EditProjectBoardForm{}), repo.EditProjectBoard)
|
m.Put("", web.Bind(forms.EditProjectColumnForm{}), repo.EditProjectColumn)
|
||||||
m.Delete("", repo.DeleteProjectBoard)
|
m.Delete("", repo.DeleteProjectColumn)
|
||||||
m.Post("/default", repo.SetDefaultProjectBoard)
|
m.Post("/default", repo.SetDefaultProjectColumn)
|
||||||
|
|
||||||
m.Post("/move", repo.MoveIssues)
|
m.Post("/move", repo.MoveIssues)
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
@ -141,18 +140,19 @@ func (r *jobStatusResolver) resolve() map[int64]actions_model.Status {
|
||||||
if allSucceed {
|
if allSucceed {
|
||||||
ret[id] = actions_model.StatusWaiting
|
ret[id] = actions_model.StatusWaiting
|
||||||
} else {
|
} else {
|
||||||
// If a job's "if" condition is "always()", the job should always run even if some of its dependencies did not succeed.
|
// Check if the job has an "if" condition
|
||||||
// See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds
|
hasIf := false
|
||||||
always := false
|
|
||||||
if wfJobs, _ := jobparser.Parse(r.jobMap[id].WorkflowPayload); len(wfJobs) == 1 {
|
if wfJobs, _ := jobparser.Parse(r.jobMap[id].WorkflowPayload); len(wfJobs) == 1 {
|
||||||
_, wfJob := wfJobs[0].Job()
|
_, wfJob := wfJobs[0].Job()
|
||||||
expr := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(wfJob.If.Value, "${{"), "}}"))
|
hasIf = len(wfJob.If.Value) > 0
|
||||||
always = expr == "always()"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if always {
|
if hasIf {
|
||||||
|
// act_runner will check the "if" condition
|
||||||
ret[id] = actions_model.StatusWaiting
|
ret[id] = actions_model.StatusWaiting
|
||||||
} else {
|
} else {
|
||||||
|
// If the "if" condition is empty and not all dependent jobs completed successfully,
|
||||||
|
// the job should be skipped.
|
||||||
ret[id] = actions_model.StatusSkipped
|
ret[id] = actions_model.StatusSkipped
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,9 +71,9 @@ func Test_jobStatusResolver_Resolve(t *testing.T) {
|
||||||
want: map[int64]actions_model.Status{},
|
want: map[int64]actions_model.Status{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with ${{ always() }} condition",
|
name: "`if` is not empty and all jobs in `needs` completed successfully",
|
||||||
jobs: actions_model.ActionJobList{
|
jobs: actions_model.ActionJobList{
|
||||||
{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
|
{ID: 1, JobID: "job1", Status: actions_model.StatusSuccess, Needs: []string{}},
|
||||||
{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
||||||
`
|
`
|
||||||
name: test
|
name: test
|
||||||
|
@ -82,15 +82,15 @@ jobs:
|
||||||
job2:
|
job2:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: job1
|
needs: job1
|
||||||
if: ${{ always() }}
|
if: ${{ always() && needs.job1.result == 'success' }}
|
||||||
steps:
|
steps:
|
||||||
- run: echo "always run"
|
- run: echo "will be checked by act_runner"
|
||||||
`)},
|
`)},
|
||||||
},
|
},
|
||||||
want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
|
want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with always() condition",
|
name: "`if` is not empty and not all jobs in `needs` completed successfully",
|
||||||
jobs: actions_model.ActionJobList{
|
jobs: actions_model.ActionJobList{
|
||||||
{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
|
{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
|
||||||
{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
||||||
|
@ -101,15 +101,15 @@ jobs:
|
||||||
job2:
|
job2:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: job1
|
needs: job1
|
||||||
if: always()
|
if: ${{ always() && needs.job1.result == 'failure' }}
|
||||||
steps:
|
steps:
|
||||||
- run: echo "always run"
|
- run: echo "will be checked by act_runner"
|
||||||
`)},
|
`)},
|
||||||
},
|
},
|
||||||
want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
|
want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "without always() condition",
|
name: "`if` is empty and not all jobs in `needs` completed successfully",
|
||||||
jobs: actions_model.ActionJobList{
|
jobs: actions_model.ActionJobList{
|
||||||
{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
|
{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
|
||||||
{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
||||||
|
@ -121,7 +121,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: job1
|
needs: job1
|
||||||
steps:
|
steps:
|
||||||
- run: echo "not always run"
|
- run: echo "should be skipped"
|
||||||
`)},
|
`)},
|
||||||
},
|
},
|
||||||
want: map[int64]actions_model.Status{2: actions_model.StatusSkipped},
|
want: map[int64]actions_model.Status{2: actions_model.StatusSkipped},
|
||||||
|
|
|
@ -31,15 +31,15 @@ func ToAPIIssue(ctx context.Context, doer *user_model.User, issue *issues_model.
|
||||||
}
|
}
|
||||||
|
|
||||||
func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) *api.Issue {
|
func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) *api.Issue {
|
||||||
if err := issue.LoadLabels(ctx); err != nil {
|
|
||||||
return &api.Issue{}
|
|
||||||
}
|
|
||||||
if err := issue.LoadPoster(ctx); err != nil {
|
if err := issue.LoadPoster(ctx); err != nil {
|
||||||
return &api.Issue{}
|
return &api.Issue{}
|
||||||
}
|
}
|
||||||
if err := issue.LoadRepo(ctx); err != nil {
|
if err := issue.LoadRepo(ctx); err != nil {
|
||||||
return &api.Issue{}
|
return &api.Issue{}
|
||||||
}
|
}
|
||||||
|
if err := issue.LoadAttachments(ctx); err != nil {
|
||||||
|
return &api.Issue{}
|
||||||
|
}
|
||||||
|
|
||||||
apiIssue := &api.Issue{
|
apiIssue := &api.Issue{
|
||||||
ID: issue.ID,
|
ID: issue.ID,
|
||||||
|
@ -63,6 +63,9 @@ func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Iss
|
||||||
}
|
}
|
||||||
apiIssue.URL = issue.APIURL(ctx)
|
apiIssue.URL = issue.APIURL(ctx)
|
||||||
apiIssue.HTMLURL = issue.HTMLURL()
|
apiIssue.HTMLURL = issue.HTMLURL()
|
||||||
|
if err := issue.LoadLabels(ctx); err != nil {
|
||||||
|
return &api.Issue{}
|
||||||
|
}
|
||||||
apiIssue.Labels = ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner)
|
apiIssue.Labels = ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner)
|
||||||
apiIssue.Repo = &api.RepositoryMeta{
|
apiIssue.Repo = &api.RepositoryMeta{
|
||||||
ID: issue.Repo.ID,
|
ID: issue.Repo.ID,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/cache"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
@ -44,7 +45,16 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
p, err := access_model.GetUserRepoPermission(ctx, pr.BaseRepo, doer)
|
var doerID int64
|
||||||
|
if doer != nil {
|
||||||
|
doerID = doer.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoDoerPermCacheKey = "repo_doer_perm_cache"
|
||||||
|
p, err := cache.GetWithContextCache(ctx, repoDoerPermCacheKey, fmt.Sprintf("%d_%d", pr.BaseRepoID, doerID),
|
||||||
|
func() (access_model.Permission, error) {
|
||||||
|
return access_model.GetUserRepoPermission(ctx, pr.BaseRepo, doer)
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetUserRepoPermission[%d]: %v", pr.BaseRepoID, err)
|
log.Error("GetUserRepoPermission[%d]: %v", pr.BaseRepoID, err)
|
||||||
p.AccessMode = perm.AccessModeNone
|
p.AccessMode = perm.AccessModeNone
|
||||||
|
|
|
@ -237,6 +237,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
||||||
MirrorInterval: mirrorInterval,
|
MirrorInterval: mirrorInterval,
|
||||||
MirrorUpdated: mirrorUpdated,
|
MirrorUpdated: mirrorUpdated,
|
||||||
RepoTransfer: transfer,
|
RepoTransfer: transfer,
|
||||||
|
Topics: repo.Topics,
|
||||||
ObjectFormatName: repo.ObjectFormatName,
|
ObjectFormatName: repo.ObjectFormatName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -370,45 +370,21 @@ func (i IssueLockForm) HasValidReason() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// __________ __ __
|
|
||||||
// \______ \_______ ____ |__| ____ _____/ |_ ______
|
|
||||||
// | ___/\_ __ \/ _ \ | |/ __ \_/ ___\ __\/ ___/
|
|
||||||
// | | | | \( <_> ) | \ ___/\ \___| | \___ \
|
|
||||||
// |____| |__| \____/\__| |\___ >\___ >__| /____ >
|
|
||||||
// \______| \/ \/ \/
|
|
||||||
|
|
||||||
// CreateProjectForm form for creating a project
|
// CreateProjectForm form for creating a project
|
||||||
type CreateProjectForm struct {
|
type CreateProjectForm struct {
|
||||||
Title string `binding:"Required;MaxSize(100)"`
|
Title string `binding:"Required;MaxSize(100)"`
|
||||||
Content string
|
Content string
|
||||||
BoardType project_model.BoardType
|
TemplateType project_model.TemplateType
|
||||||
CardType project_model.CardType
|
CardType project_model.CardType
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserCreateProjectForm is a from for creating an individual or organization
|
// EditProjectColumnForm is a form for editing a project column
|
||||||
// form.
|
type EditProjectColumnForm struct {
|
||||||
type UserCreateProjectForm struct {
|
|
||||||
Title string `binding:"Required;MaxSize(100)"`
|
|
||||||
Content string
|
|
||||||
BoardType project_model.BoardType
|
|
||||||
CardType project_model.CardType
|
|
||||||
UID int64 `binding:"Required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// EditProjectBoardForm is a form for editing a project board
|
|
||||||
type EditProjectBoardForm struct {
|
|
||||||
Title string `binding:"Required;MaxSize(100)"`
|
Title string `binding:"Required;MaxSize(100)"`
|
||||||
Sorting int8
|
Sorting int8
|
||||||
Color string `binding:"MaxSize(7)"`
|
Color string `binding:"MaxSize(7)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// _____ .__.__ __
|
|
||||||
// / \ |__| | ____ _______/ |_ ____ ____ ____
|
|
||||||
// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
|
|
||||||
// / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/
|
|
||||||
// \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ >
|
|
||||||
// \/ \/ \/ \/ \/
|
|
||||||
|
|
||||||
// CreateMilestoneForm form for creating milestone
|
// CreateMilestoneForm form for creating milestone
|
||||||
type CreateMilestoneForm struct {
|
type CreateMilestoneForm struct {
|
||||||
Title string `binding:"Required;MaxSize(50)"`
|
Title string `binding:"Required;MaxSize(50)"`
|
||||||
|
@ -422,13 +398,6 @@ func (f *CreateMilestoneForm) Validate(req *http.Request, errs binding.Errors) b
|
||||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
// .____ ___. .__
|
|
||||||
// | | _____ \_ |__ ____ | |
|
|
||||||
// | | \__ \ | __ \_/ __ \| |
|
|
||||||
// | |___ / __ \| \_\ \ ___/| |__
|
|
||||||
// |_______ (____ /___ /\___ >____/
|
|
||||||
// \/ \/ \/ \/
|
|
||||||
|
|
||||||
// CreateLabelForm form for creating label
|
// CreateLabelForm form for creating label
|
||||||
type CreateLabelForm struct {
|
type CreateLabelForm struct {
|
||||||
ID int64
|
ID int64
|
||||||
|
@ -456,13 +425,6 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors)
|
||||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
// __________ .__ .__ __________ __
|
|
||||||
// \______ \__ __| | | | \______ \ ____ ________ __ ____ _______/ |_
|
|
||||||
// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\
|
|
||||||
// | | | | / |_| |__ | | \ ___< <_| | | /\ ___/ \___ \ | |
|
|
||||||
// |____| |____/|____/____/ |____|_ /\___ >__ |____/ \___ >____ > |__|
|
|
||||||
// \/ \/ |__| \/ \/
|
|
||||||
|
|
||||||
// MergePullRequestForm form for merging Pull Request
|
// MergePullRequestForm form for merging Pull Request
|
||||||
// swagger:model MergePullRequestOption
|
// swagger:model MergePullRequestOption
|
||||||
type MergePullRequestForm struct {
|
type MergePullRequestForm struct {
|
||||||
|
|
|
@ -65,7 +65,7 @@ var hiddenCommentTypeGroups = hiddenCommentTypeGroupsType{
|
||||||
},
|
},
|
||||||
"project": {
|
"project": {
|
||||||
/*30*/ issues_model.CommentTypeProject,
|
/*30*/ issues_model.CommentTypeProject,
|
||||||
/*31*/ issues_model.CommentTypeProjectBoard,
|
/*31*/ issues_model.CommentTypeProjectColumn,
|
||||||
},
|
},
|
||||||
"issue_ref": {
|
"issue_ref": {
|
||||||
/*33*/ issues_model.CommentTypeChangeIssueRef,
|
/*33*/ issues_model.CommentTypeChangeIssueRef,
|
||||||
|
|
|
@ -39,7 +39,8 @@ func TestDeleteNotPassedAssignee(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Empty(t, issue.Assignees)
|
assert.Empty(t, issue.Assignees)
|
||||||
|
|
||||||
// Check they're gone
|
// Reload to check they're gone
|
||||||
|
issue.ResetAttributesLoaded()
|
||||||
assert.NoError(t, issue.LoadAssignees(db.DefaultContext))
|
assert.NoError(t, issue.LoadAssignees(db.DefaultContext))
|
||||||
assert.Empty(t, issue.Assignees)
|
assert.Empty(t, issue.Assignees)
|
||||||
assert.Empty(t, issue.Assignee)
|
assert.Empty(t, issue.Assignee)
|
||||||
|
|
|
@ -74,7 +74,7 @@ func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_m
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateComment updates information of comment.
|
// UpdateComment updates information of comment.
|
||||||
func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_model.User, oldContent string) error {
|
func UpdateComment(ctx context.Context, c *issues_model.Comment, contentVersion int, doer *user_model.User, oldContent string) error {
|
||||||
needsContentHistory := c.Content != oldContent && c.Type.HasContentSupport()
|
needsContentHistory := c.Content != oldContent && c.Type.HasContentSupport()
|
||||||
if needsContentHistory {
|
if needsContentHistory {
|
||||||
hasContentHistory, err := issues_model.HasIssueContentHistory(ctx, c.IssueID, c.ID)
|
hasContentHistory, err := issues_model.HasIssueContentHistory(ctx, c.IssueID, c.ID)
|
||||||
|
@ -89,7 +89,7 @@ func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_mode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := issues_model.UpdateComment(ctx, c, doer); err != nil {
|
if err := issues_model.UpdateComment(ctx, c, contentVersion, doer); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// ChangeContent changes issue content, as the given user.
|
// ChangeContent changes issue content, as the given user.
|
||||||
func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) (err error) {
|
func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string, contentVersion int) (err error) {
|
||||||
oldContent := issue.Content
|
oldContent := issue.Content
|
||||||
|
|
||||||
if err := issues_model.ChangeIssueContent(ctx, issue, doer, content); err != nil {
|
if err := issues_model.ChangeIssueContent(ctx, issue, doer, content, contentVersion); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -296,7 +296,7 @@ func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames,
|
||||||
if _, err := git_model.UpdateBranch(ctx, repoID, pusherID, branchName, commit); err != nil {
|
if _, err := git_model.UpdateBranch(ctx, repoID, pusherID, branchName, commit); err != nil {
|
||||||
return fmt.Errorf("git_model.UpdateBranch %d:%s failed: %v", repoID, branchName, err)
|
return fmt.Errorf("git_model.UpdateBranch %d:%s failed: %v", repoID, branchName, err)
|
||||||
}
|
}
|
||||||
return nil
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// if database have branches but not this branch, it means this is a new branch
|
// if database have branches but not this branch, it means this is a new branch
|
||||||
|
|
|
@ -25,11 +25,11 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ctx.Locale.Tr "repo.projects.template.desc"}}</label>
|
<label>{{ctx.Locale.Tr "repo.projects.template.desc"}}</label>
|
||||||
<div class="ui selection dropdown">
|
<div class="ui selection dropdown">
|
||||||
<input type="hidden" name="board_type" value="{{.type}}">
|
<input type="hidden" name="template_type" value="{{.type}}">
|
||||||
<div class="default text">{{ctx.Locale.Tr "repo.projects.template.desc_helper"}}</div>
|
<div class="default text">{{ctx.Locale.Tr "repo.projects.template.desc_helper"}}</div>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
{{range $element := .BoardTypes}}
|
{{range $element := .TemplateConfigs}}
|
||||||
<div class="item" data-id="{{$element.BoardType}}" data-value="{{$element.BoardType}}">{{ctx.Locale.Tr $element.Translation}}</div>
|
<div class="item" data-id="{{$element.TemplateType}}" data-value="{{$element.TemplateType}}">{{ctx.Locale.Tr $element.Translation}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
|
<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
|
||||||
<div class="edit-content-zone tw-hidden" data-update-url="{{$.root.RepoLink}}/comments/{{.ID}}" data-context="{{$.root.RepoLink}}" data-attachment-url="{{$.root.RepoLink}}/comments/{{.ID}}/attachments"></div>
|
<div class="edit-content-zone tw-hidden" data-update-url="{{$.root.RepoLink}}/comments/{{.ID}}" data-content-version="{{.ContentVersion}}" data-context="{{$.root.RepoLink}}" data-attachment-url="{{$.root.RepoLink}}/comments/{{.ID}}/attachments"></div>
|
||||||
{{if .Attachments}}
|
{{if .Attachments}}
|
||||||
{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
|
{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -131,7 +131,7 @@
|
||||||
|
|
||||||
{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects)}}
|
{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects)}}
|
||||||
<a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item">
|
<a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item">
|
||||||
{{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project_board"}}
|
{{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project"}}
|
||||||
{{if .Repository.NumOpenProjects}}
|
{{if .Repository.NumOpenProjects}}
|
||||||
<span class="ui small label">{{CountFmt .Repository.NumOpenProjects}}</span>
|
<span class="ui small label">{{CountFmt .Repository.NumOpenProjects}}</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
<!-- Projects -->
|
<!-- Projects -->
|
||||||
<div class="ui{{if not (or .OpenProjects .ClosedProjects)}} disabled{{end}} dropdown jump item">
|
<div class="ui{{if not (or .OpenProjects .ClosedProjects)}} disabled{{end}} dropdown jump item">
|
||||||
<span class="text">
|
<span class="text">
|
||||||
{{ctx.Locale.Tr "repo.project_board"}}
|
{{ctx.Locale.Tr "repo.projects"}}
|
||||||
</span>
|
</span>
|
||||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div id="issue-{{.Issue.ID}}-raw" class="raw-content tw-hidden">{{.Issue.Content}}</div>
|
<div id="issue-{{.Issue.ID}}-raw" class="raw-content tw-hidden">{{.Issue.Content}}</div>
|
||||||
<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-context="{{.RepoLink}}" data-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/attachments" data-view-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/view-attachments"></div>
|
<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-content-version="{{.Issue.ContentVersion}}" data-context="{{.RepoLink}}" data-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/attachments" data-view-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/view-attachments"></div>
|
||||||
{{if .Issue.Attachments}}
|
{{if .Issue.Attachments}}
|
||||||
{{template "repo/issue/view_content/attachments" dict "Attachments" .Issue.Attachments "RenderedContent" .Issue.RenderedContent}}
|
{{template "repo/issue/view_content/attachments" dict "Attachments" .Issue.Attachments "RenderedContent" .Issue.RenderedContent}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -67,7 +67,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
|
<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
|
||||||
<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
|
<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-content-version="{{.ContentVersion}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
|
||||||
{{if .Attachments}}
|
{{if .Attachments}}
|
||||||
{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
|
{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -441,7 +441,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
|
<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
|
||||||
<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
|
<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-content-version="{{.ContentVersion}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
|
||||||
{{if .Attachments}}
|
{{if .Attachments}}
|
||||||
{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
|
{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -93,7 +93,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
|
<div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div>
|
||||||
<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
|
<div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-content-version="{{.ContentVersion}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div>
|
||||||
{{if .Attachments}}
|
{{if .Attachments}}
|
||||||
{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
|
{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
{{$isProjectsEnabled := .Repository.UnitEnabled $.Context $.UnitTypeProjects}}
|
{{$isProjectsEnabled := .Repository.UnitEnabled $.Context $.UnitTypeProjects}}
|
||||||
{{$isProjectsGlobalDisabled := .UnitTypeProjects.UnitGlobalDisabled}}
|
{{$isProjectsGlobalDisabled := .UnitTypeProjects.UnitGlobalDisabled}}
|
||||||
<div class="inline field">
|
<div class="inline field">
|
||||||
<label>{{ctx.Locale.Tr "repo.project_board"}}</label>
|
<label>{{ctx.Locale.Tr "repo.projects"}}</label>
|
||||||
<div class="ui checkbox{{if $isProjectsGlobalDisabled}} disabled{{end}}"{{if $isProjectsGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
<div class="ui checkbox{{if $isProjectsGlobalDisabled}} disabled{{end}}"{{if $isProjectsGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
|
||||||
<input class="enable-system" name="enable_projects" type="checkbox" {{if $isProjectsEnabled}}checked{{end}}>
|
<input class="enable-system" name="enable_projects" type="checkbox" {{if $isProjectsEnabled}}checked{{end}}>
|
||||||
<label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label>
|
<label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label>
|
||||||
|
|
7
templates/swagger/v1_json.tmpl
generated
7
templates/swagger/v1_json.tmpl
generated
|
@ -24234,6 +24234,13 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"x-go-name": "Template"
|
"x-go-name": "Template"
|
||||||
},
|
},
|
||||||
|
"topics": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"x-go-name": "Topics"
|
||||||
|
},
|
||||||
"updated_at": {
|
"updated_at": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
|
|
|
@ -167,6 +167,24 @@ func doGitPushTestRepositoryFail(dstPath string, args ...string) func(*testing.T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func doGitAddSomeCommits(dstPath, branch string) func(*testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
doGitCheckoutBranch(dstPath, branch)(t)
|
||||||
|
|
||||||
|
assert.NoError(t, os.WriteFile(filepath.Join(dstPath, fmt.Sprintf("file-%s.txt", branch)), []byte(fmt.Sprintf("file %s", branch)), 0o644))
|
||||||
|
assert.NoError(t, git.AddChanges(dstPath, true))
|
||||||
|
signature := git.Signature{
|
||||||
|
Email: "test@test.test",
|
||||||
|
Name: "test",
|
||||||
|
}
|
||||||
|
assert.NoError(t, git.CommitChanges(dstPath, git.CommitChangesOptions{
|
||||||
|
Committer: &signature,
|
||||||
|
Author: &signature,
|
||||||
|
Message: fmt.Sprintf("update %s", branch),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func doGitCreateBranch(dstPath, branch string) func(*testing.T) {
|
func doGitCreateBranch(dstPath, branch string) func(*testing.T) {
|
||||||
return func(t *testing.T) {
|
return func(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
|
@ -51,6 +51,41 @@ func testGitPush(t *testing.T, u *url.URL) {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Push branches exists", func(t *testing.T) {
|
||||||
|
runTestGitPush(t, u, objectFormat, func(t *testing.T, gitPath string) (pushed, deleted []string) {
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
branchName := fmt.Sprintf("branch-%d", i)
|
||||||
|
if i < 5 {
|
||||||
|
pushed = append(pushed, branchName)
|
||||||
|
}
|
||||||
|
doGitCreateBranch(gitPath, branchName)(t)
|
||||||
|
}
|
||||||
|
// only push master and the first 5 branches
|
||||||
|
pushed = append(pushed, "master")
|
||||||
|
args := append([]string{"origin"}, pushed...)
|
||||||
|
doGitPushTestRepository(gitPath, args...)(t)
|
||||||
|
|
||||||
|
pushed = pushed[:0]
|
||||||
|
// do some changes for the first 5 branches created above
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
branchName := fmt.Sprintf("branch-%d", i)
|
||||||
|
pushed = append(pushed, branchName)
|
||||||
|
|
||||||
|
doGitAddSomeCommits(gitPath, branchName)(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 5; i < 10; i++ {
|
||||||
|
pushed = append(pushed, fmt.Sprintf("branch-%d", i))
|
||||||
|
}
|
||||||
|
pushed = append(pushed, "master")
|
||||||
|
|
||||||
|
// push all, so that master are not chagned
|
||||||
|
doGitPushTestRepository(gitPath, "origin", "--all")(t)
|
||||||
|
|
||||||
|
return pushed, deleted
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Push branches one by one", func(t *testing.T) {
|
t.Run("Push branches one by one", func(t *testing.T) {
|
||||||
runTestGitPush(t, u, objectFormat, func(t *testing.T, gitPath string) (pushed, deleted []string) {
|
runTestGitPush(t, u, objectFormat, func(t *testing.T, gitPath string) (pushed, deleted []string) {
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
|
|
|
@ -282,6 +282,34 @@ func TestIssueDependencies(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEditIssue(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
|
||||||
|
|
||||||
|
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/content", issueURL), map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, issueURL),
|
||||||
|
"content": "modified content",
|
||||||
|
"context": fmt.Sprintf("/%s/%s", "user2", "repo1"),
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/content", issueURL), map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, issueURL),
|
||||||
|
"content": "modified content",
|
||||||
|
"context": fmt.Sprintf("/%s/%s", "user2", "repo1"),
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusBadRequest)
|
||||||
|
|
||||||
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/content", issueURL), map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, issueURL),
|
||||||
|
"content": "modified content",
|
||||||
|
"content_version": "1",
|
||||||
|
"context": fmt.Sprintf("/%s/%s", "user2", "repo1"),
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
func TestIssueCommentClose(t *testing.T) {
|
func TestIssueCommentClose(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
session := loginUser(t, "user2")
|
session := loginUser(t, "user2")
|
||||||
|
@ -399,8 +427,9 @@ func TestIssueCommentUpdate(t *testing.T) {
|
||||||
|
|
||||||
// make the comment empty
|
// make the comment empty
|
||||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
|
||||||
"_csrf": GetCSRF(t, session, issueURL),
|
"_csrf": GetCSRF(t, session, issueURL),
|
||||||
"content": "",
|
"content": "",
|
||||||
|
"content_version": fmt.Sprintf("%d", comment.ContentVersion),
|
||||||
})
|
})
|
||||||
session.MakeRequest(t, req, http.StatusOK)
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
@ -408,6 +437,44 @@ func TestIssueCommentUpdate(t *testing.T) {
|
||||||
assert.Equal(t, "", comment.Content)
|
assert.Equal(t, "", comment.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIssueCommentUpdateSimultaneously(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
|
||||||
|
comment1 := "Test comment 1"
|
||||||
|
commentID := testIssueAddComment(t, session, issueURL, comment1, "")
|
||||||
|
|
||||||
|
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
|
||||||
|
assert.Equal(t, comment1, comment.Content)
|
||||||
|
|
||||||
|
modifiedContent := comment.Content + "MODIFIED"
|
||||||
|
|
||||||
|
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, issueURL),
|
||||||
|
"content": modifiedContent,
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
modifiedContent = comment.Content + "2"
|
||||||
|
|
||||||
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, issueURL),
|
||||||
|
"content": modifiedContent,
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusBadRequest)
|
||||||
|
|
||||||
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, issueURL),
|
||||||
|
"content": modifiedContent,
|
||||||
|
"content_version": "1",
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
|
||||||
|
assert.Equal(t, modifiedContent, comment.Content)
|
||||||
|
assert.Equal(t, 2, comment.ContentVersion)
|
||||||
|
}
|
||||||
|
|
||||||
func TestIssueReaction(t *testing.T) {
|
func TestIssueReaction(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
session := loginUser(t, "user2")
|
session := loginUser(t, "user2")
|
||||||
|
|
|
@ -35,23 +35,23 @@ func TestMoveRepoProjectColumns(t *testing.T) {
|
||||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||||
|
|
||||||
project1 := project_model.Project{
|
project1 := project_model.Project{
|
||||||
Title: "new created project",
|
Title: "new created project",
|
||||||
RepoID: repo2.ID,
|
RepoID: repo2.ID,
|
||||||
Type: project_model.TypeRepository,
|
Type: project_model.TypeRepository,
|
||||||
BoardType: project_model.BoardTypeNone,
|
TemplateType: project_model.TemplateTypeNone,
|
||||||
}
|
}
|
||||||
err := project_model.NewProject(db.DefaultContext, &project1)
|
err := project_model.NewProject(db.DefaultContext, &project1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
err = project_model.NewBoard(db.DefaultContext, &project_model.Board{
|
err = project_model.NewColumn(db.DefaultContext, &project_model.Column{
|
||||||
Title: fmt.Sprintf("column %d", i+1),
|
Title: fmt.Sprintf("column %d", i+1),
|
||||||
ProjectID: project1.ID,
|
ProjectID: project1.ID,
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
columns, err := project1.GetBoards(db.DefaultContext)
|
columns, err := project1.GetColumns(db.DefaultContext)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, columns, 3)
|
assert.Len(t, columns, 3)
|
||||||
assert.EqualValues(t, 0, columns[0].Sorting)
|
assert.EqualValues(t, 0, columns[0].Sorting)
|
||||||
|
@ -72,7 +72,7 @@ func TestMoveRepoProjectColumns(t *testing.T) {
|
||||||
})
|
})
|
||||||
sess.MakeRequest(t, req, http.StatusOK)
|
sess.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
columnsAfter, err := project1.GetBoards(db.DefaultContext)
|
columnsAfter, err := project1.GetColumns(db.DefaultContext)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, columns, 3)
|
assert.Len(t, columns, 3)
|
||||||
assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID)
|
assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID)
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-column {
|
.project-column {
|
||||||
background-color: var(--color-project-board-bg) !important;
|
background-color: var(--color-project-column-bg) !important;
|
||||||
border: 1px solid var(--color-secondary) !important;
|
border: 1px solid var(--color-secondary) !important;
|
||||||
margin: 0 0.5rem !important;
|
margin: 0 0.5rem !important;
|
||||||
padding: 0.5rem !important;
|
padding: 0.5rem !important;
|
||||||
|
|
|
@ -218,7 +218,7 @@
|
||||||
--color-expand-button: #2b353e;
|
--color-expand-button: #2b353e;
|
||||||
--color-placeholder-text: var(--color-text-light-3);
|
--color-placeholder-text: var(--color-text-light-3);
|
||||||
--color-editor-line-highlight: var(--color-primary-light-5);
|
--color-editor-line-highlight: var(--color-primary-light-5);
|
||||||
--color-project-board-bg: var(--color-secondary-light-2);
|
--color-project-column-bg: var(--color-secondary-light-2);
|
||||||
--color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */
|
--color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */
|
||||||
--color-reaction-bg: #e8e8ff12;
|
--color-reaction-bg: #e8e8ff12;
|
||||||
--color-reaction-hover-bg: var(--color-primary-light-4);
|
--color-reaction-hover-bg: var(--color-primary-light-4);
|
||||||
|
|
|
@ -218,7 +218,7 @@
|
||||||
--color-expand-button: #cfe8fa;
|
--color-expand-button: #cfe8fa;
|
||||||
--color-placeholder-text: var(--color-text-light-3);
|
--color-placeholder-text: var(--color-text-light-3);
|
||||||
--color-editor-line-highlight: var(--color-primary-light-6);
|
--color-editor-line-highlight: var(--color-primary-light-6);
|
||||||
--color-project-board-bg: var(--color-secondary-light-4);
|
--color-project-column-bg: var(--color-secondary-light-4);
|
||||||
--color-caret: var(--color-text-dark);
|
--color-caret: var(--color-text-dark);
|
||||||
--color-reaction-bg: #0000170a;
|
--color-reaction-bg: #0000170a;
|
||||||
--color-reaction-hover-bg: var(--color-primary-light-5);
|
--color-reaction-hover-bg: var(--color-primary-light-5);
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {initCitationFileCopyContent} from './citation.js';
|
||||||
import {initCompLabelEdit} from './comp/LabelEdit.js';
|
import {initCompLabelEdit} from './comp/LabelEdit.js';
|
||||||
import {initRepoDiffConversationNav} from './repo-diff.js';
|
import {initRepoDiffConversationNav} from './repo-diff.js';
|
||||||
import {createDropzone} from './dropzone.js';
|
import {createDropzone} from './dropzone.js';
|
||||||
|
import {showErrorToast} from '../modules/toast.js';
|
||||||
import {initCommentContent, initMarkupContent} from '../markup/content.js';
|
import {initCommentContent, initMarkupContent} from '../markup/content.js';
|
||||||
import {initCompReactionSelector} from './comp/ReactionSelector.js';
|
import {initCompReactionSelector} from './comp/ReactionSelector.js';
|
||||||
import {initRepoSettingBranches} from './repo-settings.js';
|
import {initRepoSettingBranches} from './repo-settings.js';
|
||||||
|
@ -431,11 +432,17 @@ async function onEditContent(event) {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
content: comboMarkdownEditor.value(),
|
content: comboMarkdownEditor.value(),
|
||||||
context: editContentZone.getAttribute('data-context'),
|
context: editContentZone.getAttribute('data-context'),
|
||||||
|
content_version: editContentZone.getAttribute('data-content-version'),
|
||||||
});
|
});
|
||||||
for (const fileInput of dropzoneInst?.element.querySelectorAll('.files [name=files]')) params.append('files[]', fileInput.value);
|
for (const fileInput of dropzoneInst?.element.querySelectorAll('.files [name=files]')) params.append('files[]', fileInput.value);
|
||||||
|
|
||||||
const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params});
|
const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
if (response.status === 400) {
|
||||||
|
showErrorToast(data.errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editContentZone.setAttribute('data-content-version', data.contentVersion);
|
||||||
if (!data.content) {
|
if (!data.content) {
|
||||||
renderContent.innerHTML = document.getElementById('no-content').innerHTML;
|
renderContent.innerHTML = document.getElementById('no-content').innerHTML;
|
||||||
rawContent.textContent = '';
|
rawContent.textContent = '';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {POST} from '../modules/fetch.js';
|
import {POST} from '../modules/fetch.js';
|
||||||
|
import {showErrorToast} from '../modules/toast.js';
|
||||||
|
|
||||||
const preventListener = (e) => e.preventDefault();
|
const preventListener = (e) => e.preventDefault();
|
||||||
|
|
||||||
|
@ -54,13 +55,20 @@ export function initMarkupTasklist() {
|
||||||
const editContentZone = container.querySelector('.edit-content-zone');
|
const editContentZone = container.querySelector('.edit-content-zone');
|
||||||
const updateUrl = editContentZone.getAttribute('data-update-url');
|
const updateUrl = editContentZone.getAttribute('data-update-url');
|
||||||
const context = editContentZone.getAttribute('data-context');
|
const context = editContentZone.getAttribute('data-context');
|
||||||
|
const contentVersion = editContentZone.getAttribute('data-content-version');
|
||||||
|
|
||||||
const requestBody = new FormData();
|
const requestBody = new FormData();
|
||||||
requestBody.append('ignore_attachments', 'true');
|
requestBody.append('ignore_attachments', 'true');
|
||||||
requestBody.append('content', newContent);
|
requestBody.append('content', newContent);
|
||||||
requestBody.append('context', context);
|
requestBody.append('context', context);
|
||||||
await POST(updateUrl, {data: requestBody});
|
requestBody.append('content_version', contentVersion);
|
||||||
|
const response = await POST(updateUrl, {data: requestBody});
|
||||||
|
const data = await response.json();
|
||||||
|
if (response.status === 400) {
|
||||||
|
showErrorToast(data.errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editContentZone.setAttribute('data-content-version', data.contentVersion);
|
||||||
rawContent.textContent = newContent;
|
rawContent.textContent = newContent;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
checkbox.checked = !checkbox.checked;
|
checkbox.checked = !checkbox.checked;
|
||||||
|
|
Loading…
Reference in a new issue