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
|
||||
|
||||
package "code.gitea.io/gitea/models/project"
|
||||
func UpdateBoardSorting
|
||||
func UpdateColumnSorting
|
||||
func ChangeProjectStatus
|
||||
|
||||
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 = 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 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 = 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 secretAccessKey to connect only available when STORAGE_TYPE is `minio`
|
||||
|
|
|
@ -54,7 +54,6 @@ type FindTaskOptions struct {
|
|||
UpdatedBefore timeutil.TimeStamp
|
||||
StartedBefore timeutil.TimeStamp
|
||||
RunnerID int64
|
||||
IDOrderDesc bool
|
||||
}
|
||||
|
||||
func (opts FindTaskOptions) ToConds() builder.Cond {
|
||||
|
@ -84,8 +83,5 @@ func (opts FindTaskOptions) ToConds() builder.Cond {
|
|||
}
|
||||
|
||||
func (opts FindTaskOptions) ToOrders() string {
|
||||
if opts.IDOrderDesc {
|
||||
return "`id` DESC"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ type Statistic struct {
|
|||
Mirror, Release, AuthSource, Webhook,
|
||||
Milestone, Label, HookTask,
|
||||
Team, UpdateTask, Project,
|
||||
ProjectBoard, Attachment,
|
||||
ProjectColumn, Attachment,
|
||||
Branches, Tags, CommitStatus int64
|
||||
IssueByLabel []IssueByLabelCount
|
||||
IssueByRepository []IssueByRepositoryCount
|
||||
|
@ -115,6 +115,6 @@ func GetStatistic(ctx context.Context) (stats Statistic) {
|
|||
stats.Counter.Team, _ = e.Count(new(organization.Team))
|
||||
stats.Counter.Attachment, _ = e.Count(new(repo_model.Attachment))
|
||||
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
|
||||
}
|
||||
|
|
|
@ -88,17 +88,13 @@ func (opts FindBranchOptions) ToConds() builder.Cond {
|
|||
|
||||
func (opts FindBranchOptions) ToOrders() string {
|
||||
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 == "" {
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
|
@ -27,23 +27,27 @@ func init() {
|
|||
|
||||
// LoadAssignees load assignees of this issue.
|
||||
func (issue *Issue) LoadAssignees(ctx context.Context) (err error) {
|
||||
if issue.isAssigneeLoaded || len(issue.Assignees) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset maybe preexisting assignees
|
||||
issue.Assignees = []*user_model.User{}
|
||||
issue.Assignee = nil
|
||||
|
||||
err = db.GetEngine(ctx).Table("`user`").
|
||||
if err = db.GetEngine(ctx).Table("`user`").
|
||||
Join("INNER", "issue_assignees", "assignee_id = `user`.id").
|
||||
Where("issue_assignees.issue_id = ?", issue.ID).
|
||||
Find(&issue.Assignees)
|
||||
if err != nil {
|
||||
Find(&issue.Assignees); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issue.isAssigneeLoaded = true
|
||||
// Check if we have at least one assignee and if yes put it in as `Assignee`
|
||||
if len(issue.Assignees) > 0 {
|
||||
issue.Assignee = issue.Assignees[0]
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAssigneeIDsByIssue returns the IDs of users assigned to an issue
|
||||
|
|
|
@ -52,6 +52,8 @@ func (err ErrCommentNotExist) Unwrap() error {
|
|||
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.
|
||||
type CommentType int
|
||||
|
||||
|
@ -101,7 +103,7 @@ const (
|
|||
CommentTypePullRequestPush // 29 push to PR head branch
|
||||
|
||||
CommentTypeProject // 30 Project changed
|
||||
CommentTypeProjectBoard // 31 Project board changed
|
||||
CommentTypeProjectColumn // 31 Project column changed
|
||||
|
||||
CommentTypeDismissReview // 32 Dismiss Review
|
||||
|
||||
|
@ -146,7 +148,7 @@ var commentStrings = []string{
|
|||
"merge_pull",
|
||||
"pull_push",
|
||||
"project",
|
||||
"project_board",
|
||||
"project_board", // FIXME: the name should be project_column
|
||||
"dismiss_review",
|
||||
"change_issue_ref",
|
||||
"pull_scheduled_merge",
|
||||
|
@ -262,6 +264,7 @@ type Comment struct {
|
|||
Line int64 // - previous line / + proposed line
|
||||
TreePath string
|
||||
Content string `xorm:"LONGTEXT"`
|
||||
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
||||
RenderedContent template.HTML `xorm:"-"`
|
||||
|
||||
// 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.
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
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
|
||||
}
|
||||
if affected == 0 {
|
||||
return ErrCommentAlreadyChanged
|
||||
}
|
||||
if err := c.AddCrossReferences(ctx, doer, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -16,26 +16,26 @@ import (
|
|||
// CommentList defines a list of comments
|
||||
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
|
||||
func (comments CommentList) LoadPosters(ctx context.Context) error {
|
||||
if len(comments) == 0 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
if comment.Poster == nil {
|
||||
comment.Poster = getPoster(comment.PosterID, posterMaps)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -94,6 +94,8 @@ func (err ErrIssueWasClosed) Error() string {
|
|||
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.
|
||||
type Issue struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
|
@ -107,13 +109,17 @@ type Issue struct {
|
|||
Title string `xorm:"name"`
|
||||
Content string `xorm:"LONGTEXT"`
|
||||
RenderedContent template.HTML `xorm:"-"`
|
||||
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
||||
Labels []*Label `xorm:"-"`
|
||||
isLabelsLoaded bool `xorm:"-"`
|
||||
MilestoneID int64 `xorm:"INDEX"`
|
||||
Milestone *Milestone `xorm:"-"`
|
||||
isMilestoneLoaded bool `xorm:"-"`
|
||||
Project *project_model.Project `xorm:"-"`
|
||||
Priority int
|
||||
AssigneeID int64 `xorm:"-"`
|
||||
Assignee *user_model.User `xorm:"-"`
|
||||
isAssigneeLoaded bool `xorm:"-"`
|
||||
IsClosed bool `xorm:"INDEX"`
|
||||
IsRead bool `xorm:"-"`
|
||||
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
||||
|
@ -132,6 +138,7 @@ type Issue struct {
|
|||
NoAutoTime bool `xorm:"-"`
|
||||
|
||||
Attachments []*repo_model.Attachment `xorm:"-"`
|
||||
isAttachmentsLoaded bool `xorm:"-"`
|
||||
Comments CommentList `xorm:"-"`
|
||||
Reactions ReactionList `xorm:"-"`
|
||||
TotalTrackedTime int64 `xorm:"-"`
|
||||
|
@ -187,6 +194,19 @@ func (issue *Issue) LoadRepo(ctx context.Context) (err error) {
|
|||
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
|
||||
func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
|
||||
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.
|
||||
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)
|
||||
if err != nil && !IsErrMilestoneNotExist(err) {
|
||||
return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %w", issue.RepoID, issue.MilestoneID, err)
|
||||
}
|
||||
issue.isMilestoneLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -327,11 +348,8 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
if issue.Attachments == nil {
|
||||
issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)
|
||||
}
|
||||
if err = issue.LoadAttachments(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = issue.loadComments(ctx); err != nil {
|
||||
|
@ -350,6 +368,13 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
|
|||
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
|
||||
func (issue *Issue) GetIsRead(ctx context.Context, userID int64) error {
|
||||
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
|
||||
}
|
||||
|
||||
issue.isLabelsLoaded = false
|
||||
issue.Labels = nil
|
||||
if err = issue.LoadLabels(ctx); err != nil {
|
||||
return err
|
||||
|
@ -160,6 +161,8 @@ func NewIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *us
|
|||
return err
|
||||
}
|
||||
|
||||
// reload all labels
|
||||
issue.isLabelsLoaded = false
|
||||
issue.Labels = nil
|
||||
if err = issue.LoadLabels(ctx); err != nil {
|
||||
return err
|
||||
|
@ -325,11 +328,12 @@ func FixIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) {
|
|||
|
||||
// LoadLabels loads labels
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
|
||||
}
|
||||
issue.isLabelsLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -73,29 +73,29 @@ func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.Reposi
|
|||
return repo_model.ValuesRepository(repoMaps), nil
|
||||
}
|
||||
|
||||
func (issues IssueList) getPosterIDs() []int64 {
|
||||
return container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
|
||||
return issue.PosterID, true
|
||||
})
|
||||
}
|
||||
|
||||
func (issues IssueList) loadPosters(ctx context.Context) error {
|
||||
func (issues IssueList) LoadPosters(ctx context.Context) error {
|
||||
if len(issues) == 0 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
if issue.Poster == nil {
|
||||
issue.Poster = getPoster(issue.PosterID, posterMaps)
|
||||
}
|
||||
}
|
||||
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))
|
||||
left := len(posterIDs)
|
||||
for left > 0 {
|
||||
|
@ -137,7 +137,7 @@ func (issues IssueList) getIssueIDs() []int64 {
|
|||
return ids
|
||||
}
|
||||
|
||||
func (issues IssueList) loadLabels(ctx context.Context) error {
|
||||
func (issues IssueList) LoadLabels(ctx context.Context) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
@ -169,7 +169,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
|||
err = rows.Scan(&labelIssue)
|
||||
if err != 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
|
||||
}
|
||||
|
@ -178,7 +178,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
|||
// When there are no rows left and we try to close it.
|
||||
// Since that is not relevant for us, we can safely ignore it.
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadLabels: Close: %w", err1)
|
||||
return fmt.Errorf("IssueList.LoadLabels: Close: %w", err1)
|
||||
}
|
||||
left -= limit
|
||||
issueIDs = issueIDs[limit:]
|
||||
|
@ -186,6 +186,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
|||
|
||||
for _, issue := range issues {
|
||||
issue.Labels = issueLabels[issue.ID]
|
||||
issue.isLabelsLoaded = true
|
||||
}
|
||||
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()
|
||||
if len(milestoneIDs) == 0 {
|
||||
return nil
|
||||
|
@ -221,6 +222,7 @@ func (issues IssueList) loadMilestones(ctx context.Context) error {
|
|||
|
||||
for _, issue := range issues {
|
||||
issue.Milestone = milestoneMaps[issue.MilestoneID]
|
||||
issue.isMilestoneLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -264,7 +266,7 @@ func (issues IssueList) LoadProjects(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) loadAssignees(ctx context.Context) error {
|
||||
func (issues IssueList) LoadAssignees(ctx context.Context) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
@ -311,6 +313,10 @@ func (issues IssueList) loadAssignees(ctx context.Context) error {
|
|||
|
||||
for _, issue := range issues {
|
||||
issue.Assignees = assignees[issue.ID]
|
||||
if len(issue.Assignees) > 0 {
|
||||
issue.Assignee = issue.Assignees[0]
|
||||
}
|
||||
issue.isAssigneeLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -414,6 +420,7 @@ func (issues IssueList) LoadAttachments(ctx context.Context) (err error) {
|
|||
|
||||
for _, issue := range issues {
|
||||
issue.Attachments = attachments[issue.ID]
|
||||
issue.isAttachmentsLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -539,23 +546,23 @@ func (issues IssueList) LoadAttributes(ctx context.Context) error {
|
|||
return fmt.Errorf("issue.loadAttributes: LoadRepositories: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.loadPosters(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadPosters: %w", err)
|
||||
if err := issues.LoadPosters(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: LoadPosters: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.loadLabels(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadLabels: %w", err)
|
||||
if err := issues.LoadLabels(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: LoadLabels: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.loadMilestones(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadMilestones: %w", err)
|
||||
if err := issues.LoadMilestones(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: LoadMilestones: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.LoadProjects(ctx); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -37,20 +37,20 @@ func (issue *Issue) projectID(ctx context.Context) int64 {
|
|||
return ip.ProjectID
|
||||
}
|
||||
|
||||
// ProjectBoardID return project board id if issue was assigned to one
|
||||
func (issue *Issue) ProjectBoardID(ctx context.Context) int64 {
|
||||
// ProjectColumnID return project column id if issue was assigned to one
|
||||
func (issue *Issue) ProjectColumnID(ctx context.Context) int64 {
|
||||
var ip project_model.ProjectIssue
|
||||
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
|
||||
if err != nil || !has {
|
||||
return 0
|
||||
}
|
||||
return ip.ProjectBoardID
|
||||
return ip.ProjectColumnID
|
||||
}
|
||||
|
||||
// LoadIssuesFromBoard load issues assigned to this board
|
||||
func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList, error) {
|
||||
// LoadIssuesFromColumn load issues assigned to this column
|
||||
func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column) (IssueList, error) {
|
||||
issueList, err := Issues(ctx, &IssuesOptions{
|
||||
ProjectBoardID: b.ID,
|
||||
ProjectColumnID: b.ID,
|
||||
ProjectID: b.ProjectID,
|
||||
SortType: "project-column-sorting",
|
||||
})
|
||||
|
@ -60,7 +60,7 @@ func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList
|
|||
|
||||
if b.Default {
|
||||
issues, err := Issues(ctx, &IssuesOptions{
|
||||
ProjectBoardID: db.NoConditionID,
|
||||
ProjectColumnID: db.NoConditionID,
|
||||
ProjectID: b.ProjectID,
|
||||
SortType: "project-column-sorting",
|
||||
})
|
||||
|
@ -77,11 +77,11 @@ func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList
|
|||
return issueList, nil
|
||||
}
|
||||
|
||||
// LoadIssuesFromBoardList load issues assigned to the boards
|
||||
func LoadIssuesFromBoardList(ctx context.Context, bs project_model.BoardList) (map[int64]IssueList, error) {
|
||||
// LoadIssuesFromColumnList load issues assigned to the columns
|
||||
func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList) (map[int64]IssueList, error) {
|
||||
issuesMap := make(map[int64]IssueList, len(bs))
|
||||
for i := range bs {
|
||||
il, err := LoadIssuesFromBoard(ctx, bs[i])
|
||||
il, err := LoadIssuesFromColumn(ctx, bs[i])
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
if newColumnID == 0 {
|
||||
newDefaultColumn, err := newProject.GetDefaultBoard(ctx)
|
||||
newDefaultColumn, err := newProject.GetDefaultColumn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -155,7 +155,7 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
|
|||
return db.Insert(ctx, &project_model.ProjectIssue{
|
||||
IssueID: issue.ID,
|
||||
ProjectID: newProjectID,
|
||||
ProjectBoardID: newColumnID,
|
||||
ProjectColumnID: newColumnID,
|
||||
Sorting: newSorting,
|
||||
})
|
||||
})
|
||||
|
|
|
@ -33,7 +33,7 @@ type IssuesOptions struct { //nolint
|
|||
SubscriberID int64
|
||||
MilestoneIDs []int64
|
||||
ProjectID int64
|
||||
ProjectBoardID int64
|
||||
ProjectColumnID int64
|
||||
IsClosed optional.Option[bool]
|
||||
IsPull optional.Option[bool]
|
||||
LabelIDs []int64
|
||||
|
@ -169,12 +169,12 @@ func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Sessio
|
|||
return sess
|
||||
}
|
||||
|
||||
func applyProjectBoardCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
|
||||
// opts.ProjectBoardID == 0 means all project boards,
|
||||
func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
|
||||
// opts.ProjectColumnID == 0 means all project columns,
|
||||
// do not need to apply any condition
|
||||
if opts.ProjectBoardID > 0 {
|
||||
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectBoardID}))
|
||||
} else if opts.ProjectBoardID == db.NoConditionID {
|
||||
if opts.ProjectColumnID > 0 {
|
||||
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": opts.ProjectColumnID}))
|
||||
} else if opts.ProjectColumnID == db.NoConditionID {
|
||||
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0}))
|
||||
}
|
||||
return sess
|
||||
|
@ -246,7 +246,7 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
|
|||
|
||||
applyProjectCondition(sess, opts)
|
||||
|
||||
applyProjectBoardCondition(sess, opts)
|
||||
applyProjectColumnCondition(sess, opts)
|
||||
|
||||
if opts.IsPull.Has() {
|
||||
sess.And("issue.is_pull=?", opts.IsPull.Value())
|
||||
|
|
|
@ -25,17 +25,18 @@ import (
|
|||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// UpdateIssueCols updates cols of issue
|
||||
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)
|
||||
if issue.NoAutoTime {
|
||||
cols = append(cols, []string{"updated_unix"}...)
|
||||
sess.NoAutoTime()
|
||||
}
|
||||
if _, err := sess.Cols(cols...).Update(issue); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return sess.Cols(cols...).Where(cond).Update(issue)
|
||||
}
|
||||
|
||||
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.
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -269,10 +270,16 @@ func ChangeIssueContent(ctx context.Context, issue *Issue, doer *user_model.User
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if affected == 0 {
|
||||
return ErrIssueAlreadyChanged
|
||||
}
|
||||
|
||||
historyDate := timeutil.TimeStampNow()
|
||||
if issue.NoAutoTime {
|
||||
|
|
|
@ -163,6 +163,7 @@ type PullRequest struct {
|
|||
Issue *Issue `xorm:"-"`
|
||||
Index int64
|
||||
RequestedReviewers []*user_model.User `xorm:"-"`
|
||||
isRequestedReviewersLoaded bool `xorm:"-"`
|
||||
|
||||
HeadRepoID int64 `xorm:"INDEX"`
|
||||
HeadRepo *repo_model.Repository `xorm:"-"`
|
||||
|
@ -289,7 +290,7 @@ func (pr *PullRequest) LoadHeadRepo(ctx context.Context) (err error) {
|
|||
|
||||
// LoadRequestedReviewers loads the requested reviewers.
|
||||
func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
||||
if len(pr.RequestedReviewers) > 0 {
|
||||
if pr.isRequestedReviewersLoaded || len(pr.RequestedReviewers) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -297,10 +298,10 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = reviews.LoadReviewers(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
pr.isRequestedReviewersLoaded = true
|
||||
for _, review := range reviews {
|
||||
pr.RequestedReviewers = append(pr.RequestedReviewers, review.Reviewer)
|
||||
}
|
||||
|
|
|
@ -9,8 +9,10 @@ import (
|
|||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
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/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
|
||||
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 {
|
||||
opts.Page = 1
|
||||
}
|
||||
|
@ -159,27 +161,64 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
|
|||
// PullRequestList defines a list of pull requests
|
||||
type PullRequestList []*PullRequest
|
||||
|
||||
func (prs PullRequestList) LoadAttributes(ctx context.Context) error {
|
||||
if len(prs) == 0 {
|
||||
func (prs PullRequestList) getRepositoryIDs() []int64 {
|
||||
repoIDs := make(container.Set[int64])
|
||||
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()
|
||||
}
|
||||
|
||||
func (prs PullRequestList) LoadRepositories(ctx context.Context) error {
|
||||
repoIDs := prs.getRepositoryIDs()
|
||||
reposMap := make(map[int64]*repo_model.Repository, len(repoIDs))
|
||||
if err := db.GetEngine(ctx).
|
||||
In("id", repoIDs).
|
||||
Find(&reposMap); err != nil {
|
||||
return fmt.Errorf("find repos: %w", err)
|
||||
}
|
||||
for _, pr := range prs {
|
||||
if pr.BaseRepo == nil {
|
||||
pr.BaseRepo = reposMap[pr.BaseRepoID]
|
||||
}
|
||||
if pr.HeadRepo == nil {
|
||||
pr.HeadRepo = reposMap[pr.HeadRepoID]
|
||||
pr.isHeadRepoLoaded = true
|
||||
}
|
||||
}
|
||||
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([]*Issue, 0, len(issueIDs))
|
||||
issues := make(map[int64]*Issue, len(issueIDs))
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("id > 0").
|
||||
In("id", issueIDs).
|
||||
Find(&issues); err != nil {
|
||||
return fmt.Errorf("find issues: %w", err)
|
||||
return nil, fmt.Errorf("find issues: %w", err)
|
||||
}
|
||||
|
||||
set := make(map[int64]*Issue)
|
||||
for i := range issues {
|
||||
set[issues[i].ID] = issues[i]
|
||||
}
|
||||
issueList := make(IssueList, 0, len(prs))
|
||||
for _, pr := range prs {
|
||||
pr.Issue = set[pr.IssueID]
|
||||
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
|
||||
|
@ -189,20 +228,26 @@ func (prs PullRequestList) LoadAttributes(ctx context.Context) error {
|
|||
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)
|
||||
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
|
||||
}
|
||||
return nil
|
||||
issueList = append(issueList, pr.Issue)
|
||||
}
|
||||
return issueList, nil
|
||||
}
|
||||
|
||||
// GetIssueIDs returns all issue ids
|
||||
func (prs PullRequestList) GetIssueIDs() []int64 {
|
||||
issueIDs := make([]int64, 0, len(prs))
|
||||
for i := range prs {
|
||||
issueIDs = append(issueIDs, prs[i].IssueID)
|
||||
return container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) {
|
||||
if pr.Issue == nil {
|
||||
return pr.IssueID, pr.IssueID > 0
|
||||
}
|
||||
return issueIDs
|
||||
return 0, false
|
||||
})
|
||||
}
|
||||
|
||||
// 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_21"
|
||||
"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_7"
|
||||
"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),
|
||||
|
||||
// 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
|
||||
|
|
|
@ -60,7 +60,7 @@ func addObjectFormatNameToRepository(x *xorm.Engine) error {
|
|||
|
||||
// Here to catch weird edge-cases where column constraints above are
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import (
|
|||
|
||||
func Test_CheckProjectColumnsConsistency(t *testing.T) {
|
||||
// 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()
|
||||
if x == nil || t.Failed() {
|
||||
return
|
||||
|
@ -23,22 +23,22 @@ func Test_CheckProjectColumnsConsistency(t *testing.T) {
|
|||
|
||||
assert.NoError(t, CheckProjectColumnsConsistency(x))
|
||||
|
||||
// check if default board was added
|
||||
var defaultBoard project.Board
|
||||
has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultBoard)
|
||||
// check if default column was added
|
||||
var defaultColumn project.Column
|
||||
has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultColumn)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, has)
|
||||
assert.Equal(t, int64(1), defaultBoard.ProjectID)
|
||||
assert.True(t, defaultBoard.Default)
|
||||
assert.Equal(t, int64(1), defaultColumn.ProjectID)
|
||||
assert.True(t, defaultColumn.Default)
|
||||
|
||||
// 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.Equal(t, int64(2), expectDefaultBoard.ProjectID)
|
||||
assert.False(t, expectDefaultBoard.Default)
|
||||
assert.Equal(t, int64(2), expectDefaultColumn.ProjectID)
|
||||
assert.False(t, expectDefaultColumn.Default)
|
||||
|
||||
expectNonDefaultBoard, err := project.GetBoard(db.DefaultContext, 3)
|
||||
expectNonDefaultColumn, err := project.GetColumn(db.DefaultContext, 3)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), expectNonDefaultBoard.ProjectID)
|
||||
assert.True(t, expectNonDefaultBoard.Default)
|
||||
assert.Equal(t, int64(2), expectNonDefaultColumn.ProjectID)
|
||||
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"
|
||||
)
|
||||
|
||||
func TestGetDefaultBoard(t *testing.T) {
|
||||
func TestGetDefaultColumn(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
projectWithoutDefault, err := GetProjectByID(db.DefaultContext, 5)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check if default board was added
|
||||
board, err := projectWithoutDefault.GetDefaultBoard(db.DefaultContext)
|
||||
// check if default column was added
|
||||
column, err := projectWithoutDefault.GetDefaultColumn(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(5), board.ProjectID)
|
||||
assert.Equal(t, "Uncategorized", board.Title)
|
||||
assert.Equal(t, int64(5), column.ProjectID)
|
||||
assert.Equal(t, "Uncategorized", column.Title)
|
||||
|
||||
projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check if multiple defaults were removed
|
||||
board, err = projectWithMultipleDefaults.GetDefaultBoard(db.DefaultContext)
|
||||
column, err = projectWithMultipleDefaults.GetDefaultColumn(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(6), board.ProjectID)
|
||||
assert.Equal(t, int64(9), board.ID)
|
||||
assert.Equal(t, int64(6), column.ProjectID)
|
||||
assert.Equal(t, int64(9), column.ID)
|
||||
|
||||
// set 8 as default board
|
||||
assert.NoError(t, SetDefaultBoard(db.DefaultContext, board.ProjectID, 8))
|
||||
// set 8 as default column
|
||||
assert.NoError(t, SetDefaultColumn(db.DefaultContext, column.ProjectID, 8))
|
||||
|
||||
// then 9 will become a non-default board
|
||||
board, err = GetBoard(db.DefaultContext, 9)
|
||||
// then 9 will become a non-default column
|
||||
column, err = GetColumn(db.DefaultContext, 9)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(6), board.ProjectID)
|
||||
assert.False(t, board.Default)
|
||||
assert.Equal(t, int64(6), column.ProjectID)
|
||||
assert.False(t, column.Default)
|
||||
}
|
||||
|
||||
func Test_moveIssuesToAnotherColumn(t *testing.T) {
|
||||
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)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, issues, 1)
|
||||
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)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, issues, 1)
|
||||
|
@ -81,7 +81,7 @@ func Test_MoveColumnsOnProject(t *testing.T) {
|
|||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
|
||||
columns, err := project1.GetBoards(db.DefaultContext)
|
||||
columns, err := project1.GetColumns(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columns, 3)
|
||||
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)
|
||||
|
||||
columnsAfter, err := project1.GetBoards(db.DefaultContext)
|
||||
columnsAfter, err := project1.GetColumns(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columnsAfter, 3)
|
||||
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)
|
||||
}
|
||||
|
||||
func Test_NewBoard(t *testing.T) {
|
||||
func Test_NewColumn(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1})
|
||||
columns, err := project1.GetBoards(db.DefaultContext)
|
||||
columns, err := project1.GetColumns(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columns, 3)
|
||||
|
||||
for i := 0; i < maxProjectColumns-3; i++ {
|
||||
err := NewBoard(db.DefaultContext, &Board{
|
||||
Title: fmt.Sprintf("board-%d", i+4),
|
||||
err := NewColumn(db.DefaultContext, &Column{
|
||||
Title: fmt.Sprintf("column-%d", i+4),
|
||||
ProjectID: project1.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
err = NewBoard(db.DefaultContext, &Board{
|
||||
Title: "board-21",
|
||||
err = NewColumn(db.DefaultContext, &Column{
|
||||
Title: "column-21",
|
||||
ProjectID: project1.ID,
|
||||
})
|
||||
assert.Error(t, err)
|
|
@ -18,10 +18,10 @@ type ProjectIssue struct { //revive:disable-line:exported
|
|||
IssueID 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.
|
||||
ProjectBoardID int64 `xorm:"INDEX"`
|
||||
// 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.
|
||||
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"`
|
||||
}
|
||||
|
||||
|
@ -76,13 +76,13 @@ func (p *Project) NumOpenIssues(ctx context.Context) int {
|
|||
return int(c)
|
||||
}
|
||||
|
||||
// MoveIssuesOnProjectBoard 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 {
|
||||
// MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column
|
||||
func MoveIssuesOnProjectColumn(ctx context.Context, column *Column, sortedIssueIDs map[int64]int64) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
sess := db.GetEngine(ctx)
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, 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 {
|
||||
return err
|
||||
}
|
||||
|
@ -100,12 +100,12 @@ func MoveIssuesOnProjectBoard(ctx context.Context, board *Board, sortedIssueIDs
|
|||
})
|
||||
}
|
||||
|
||||
func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board) error {
|
||||
if b.ProjectID != newColumn.ProjectID {
|
||||
func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
|
||||
if c.ProjectID != newColumn.ProjectID {
|
||||
return fmt.Errorf("columns have to be in the same project")
|
||||
}
|
||||
|
||||
if b.ID == newColumn.ID {
|
||||
if c.ID == newColumn.ID {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -121,7 +121,7 @@ func (b *Board) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Board)
|
|||
return err
|
||||
}
|
||||
|
||||
issues, err := b.GetIssues(ctx)
|
||||
issues, err := c.GetIssues(ctx)
|
||||
if err != nil {
|
||||
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)
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
for i, issue := range issues {
|
||||
issue.ProjectBoardID = newColumn.ID
|
||||
issue.ProjectColumnID = newColumn.ID
|
||||
issue.Sorting = nextSorting + int64(i)
|
||||
if _, err := db.GetEngine(ctx).ID(issue.ID).Cols("project_board_id", "sorting").Update(issue); err != nil {
|
||||
return err
|
||||
|
|
|
@ -21,13 +21,7 @@ import (
|
|||
)
|
||||
|
||||
type (
|
||||
// BoardConfig is used to identify the type of board that is being created
|
||||
BoardConfig struct {
|
||||
BoardType BoardType
|
||||
Translation string
|
||||
}
|
||||
|
||||
// CardConfig is used to identify the type of board card that is being used
|
||||
// CardConfig is used to identify the type of column card that is being used
|
||||
CardConfig struct {
|
||||
CardType CardType
|
||||
Translation string
|
||||
|
@ -38,7 +32,7 @@ type (
|
|||
)
|
||||
|
||||
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
|
||||
|
||||
// TypeRepository is a project that is tied to a repository
|
||||
|
@ -68,26 +62,26 @@ func (err ErrProjectNotExist) Unwrap() error {
|
|||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
// ErrProjectBoardNotExist represents a "ProjectBoardNotExist" kind of error.
|
||||
type ErrProjectBoardNotExist struct {
|
||||
BoardID int64
|
||||
// ErrProjectColumnNotExist represents a "ErrProjectColumnNotExist" kind of error.
|
||||
type ErrProjectColumnNotExist struct {
|
||||
ColumnID int64
|
||||
}
|
||||
|
||||
// IsErrProjectBoardNotExist checks if an error is a ErrProjectBoardNotExist
|
||||
func IsErrProjectBoardNotExist(err error) bool {
|
||||
_, ok := err.(ErrProjectBoardNotExist)
|
||||
// IsErrProjectColumnNotExist checks if an error is a ErrProjectColumnNotExist
|
||||
func IsErrProjectColumnNotExist(err error) bool {
|
||||
_, ok := err.(ErrProjectColumnNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrProjectBoardNotExist) Error() string {
|
||||
return fmt.Sprintf("project board does not exist [id: %d]", err.BoardID)
|
||||
func (err ErrProjectColumnNotExist) Error() string {
|
||||
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
|
||||
}
|
||||
|
||||
// Project represents a project board
|
||||
// Project represents a project
|
||||
type Project struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Title string `xorm:"INDEX NOT NULL"`
|
||||
|
@ -98,7 +92,7 @@ type Project struct {
|
|||
Repo *repo_model.Repository `xorm:"-"`
|
||||
CreatorID int64 `xorm:"NOT NULL"`
|
||||
IsClosed bool `xorm:"INDEX"`
|
||||
BoardType BoardType
|
||||
TemplateType TemplateType `xorm:"'board_type'"` // TODO: rename the column to template_type
|
||||
CardType CardType
|
||||
Type Type
|
||||
|
||||
|
@ -172,16 +166,7 @@ func init() {
|
|||
db.RegisterModel(new(Project))
|
||||
}
|
||||
|
||||
// GetBoardConfig retrieves the types of configurations project boards 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
|
||||
// GetCardConfig retrieves the types of configurations project column cards could have
|
||||
func GetCardConfig() []CardConfig {
|
||||
return []CardConfig{
|
||||
{CardTypeTextOnly, "repo.projects.card_type.text_only"},
|
||||
|
@ -251,8 +236,8 @@ func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy {
|
|||
|
||||
// NewProject creates a new Project
|
||||
func NewProject(ctx context.Context, p *Project) error {
|
||||
if !IsBoardTypeValid(p.BoardType) {
|
||||
p.BoardType = BoardTypeNone
|
||||
if !IsTemplateTypeValid(p.TemplateType) {
|
||||
p.TemplateType = TemplateTypeNone
|
||||
}
|
||||
|
||||
if !IsCardTypeValid(p.CardType) {
|
||||
|
@ -263,12 +248,7 @@ func NewProject(ctx context.Context, p *Project) error {
|
|||
return util.NewInvalidArgumentErrorf("project type is not valid")
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if err := db.Insert(ctx, p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -279,11 +259,8 @@ func NewProject(ctx context.Context, p *Project) error {
|
|||
}
|
||||
}
|
||||
|
||||
if err := createBoardsForProjectsType(ctx, p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
return createDefaultColumnsForProject(ctx, p)
|
||||
})
|
||||
}
|
||||
|
||||
// GetProjectByID returns the projects in a repository
|
||||
|
@ -417,7 +394,7 @@ func DeleteProjectByID(ctx context.Context, id int64) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := deleteBoardByProjectID(ctx, id); err != nil {
|
||||
if err := deleteColumnByProjectID(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ func TestProject(t *testing.T) {
|
|||
|
||||
project := &Project{
|
||||
Type: TypeRepository,
|
||||
BoardType: BoardTypeBasicKanban,
|
||||
TemplateType: TemplateTypeBasicKanban,
|
||||
CardType: CardTypeTextOnly,
|
||||
Title: "New Project",
|
||||
RepoID: 1,
|
||||
|
|
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
|
||||
TypeExternalWiki // 6 ExternalWiki
|
||||
TypeExternalTracker // 7 ExternalTracker
|
||||
TypeProjects // 8 Kanban board
|
||||
TypeProjects // 8 Projects
|
||||
TypePackages // 9 Packages
|
||||
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.
|
||||
func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
users := make([]*User, 0, len(ids))
|
||||
err := db.GetEngine(ctx).In("id", ids).
|
||||
Table("user").
|
||||
|
|
|
@ -230,8 +230,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
|||
if options.ProjectID.Has() {
|
||||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectID.Value(), "project_id"))
|
||||
}
|
||||
if options.ProjectBoardID.Has() {
|
||||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectBoardID.Value(), "project_board_id"))
|
||||
if options.ProjectColumnID.Has() {
|
||||
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectColumnID.Value(), "project_board_id"))
|
||||
}
|
||||
|
||||
if options.PosterID.Has() {
|
||||
|
|
|
@ -61,7 +61,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
|
|||
ReviewedID: convertID(options.ReviewedID),
|
||||
SubscriberID: convertID(options.SubscriberID),
|
||||
ProjectID: convertID(options.ProjectID),
|
||||
ProjectBoardID: convertID(options.ProjectBoardID),
|
||||
ProjectColumnID: convertID(options.ProjectColumnID),
|
||||
IsClosed: options.IsClosed,
|
||||
IsPull: options.IsPull,
|
||||
IncludedLabelNames: nil,
|
||||
|
|
|
@ -50,7 +50,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
|||
}
|
||||
|
||||
searchOpt.ProjectID = convertID(opts.ProjectID)
|
||||
searchOpt.ProjectBoardID = convertID(opts.ProjectBoardID)
|
||||
searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID)
|
||||
searchOpt.PosterID = convertID(opts.PosterID)
|
||||
searchOpt.AssigneeID = convertID(opts.AssigneeID)
|
||||
searchOpt.MentionID = convertID(opts.MentionedID)
|
||||
|
|
|
@ -197,8 +197,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
|||
if options.ProjectID.Has() {
|
||||
query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value()))
|
||||
}
|
||||
if options.ProjectBoardID.Has() {
|
||||
query.Must(elastic.NewTermQuery("project_board_id", options.ProjectBoardID.Value()))
|
||||
if options.ProjectColumnID.Has() {
|
||||
query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value()))
|
||||
}
|
||||
|
||||
if options.PosterID.Has() {
|
||||
|
|
|
@ -369,13 +369,13 @@ func searchIssueInProject(t *testing.T) {
|
|||
},
|
||||
{
|
||||
SearchOptions{
|
||||
ProjectBoardID: optional.Some(int64(1)),
|
||||
ProjectColumnID: optional.Some(int64(1)),
|
||||
},
|
||||
[]int64{1},
|
||||
},
|
||||
{
|
||||
SearchOptions{
|
||||
ProjectBoardID: optional.Some(int64(0)), // issue with in default board
|
||||
ProjectColumnID: optional.Some(int64(0)), // issue with in default column
|
||||
},
|
||||
[]int64{2},
|
||||
},
|
||||
|
|
|
@ -27,7 +27,7 @@ type IndexerData struct {
|
|||
NoLabel bool `json:"no_label"` // True if LabelIDs is empty
|
||||
MilestoneID int64 `json:"milestone_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"`
|
||||
AssigneeID int64 `json:"assignee_id"`
|
||||
MentionIDs []int64 `json:"mention_ids"`
|
||||
|
@ -90,7 +90,7 @@ type SearchOptions struct {
|
|||
MilestoneIDs []int64 // milestones the issues have
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -352,38 +352,38 @@ var cases = []*testIndexerCase{
|
|||
},
|
||||
},
|
||||
{
|
||||
Name: "ProjectBoardID",
|
||||
Name: "ProjectColumnID",
|
||||
SearchOptions: &internal.SearchOptions{
|
||||
Paginator: &db.ListOptions{
|
||||
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) {
|
||||
assert.Equal(t, 5, len(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 {
|
||||
return v.ProjectBoardID == 1
|
||||
return v.ProjectColumnID == 1
|
||||
}), result.Total)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "no ProjectBoardID",
|
||||
Name: "no ProjectColumnID",
|
||||
SearchOptions: &internal.SearchOptions{
|
||||
Paginator: &db.ListOptions{
|
||||
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) {
|
||||
assert.Equal(t, 5, len(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 {
|
||||
return v.ProjectBoardID == 0
|
||||
return v.ProjectColumnID == 0
|
||||
}), result.Total)
|
||||
},
|
||||
},
|
||||
|
@ -720,7 +720,7 @@ func generateDefaultIndexerData() []*internal.IndexerData {
|
|||
NoLabel: len(labelIDs) == 0,
|
||||
MilestoneID: issueIndex % 4,
|
||||
ProjectID: issueIndex % 5,
|
||||
ProjectBoardID: issueIndex % 6,
|
||||
ProjectColumnID: issueIndex % 6,
|
||||
PosterID: id%10 + 1, // PosterID should not be 0
|
||||
AssigneeID: issueIndex % 10,
|
||||
MentionIDs: mentionIDs,
|
||||
|
|
|
@ -174,8 +174,8 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
|||
if options.ProjectID.Has() {
|
||||
query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value()))
|
||||
}
|
||||
if options.ProjectBoardID.Has() {
|
||||
query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectBoardID.Value()))
|
||||
if options.ProjectColumnID.Has() {
|
||||
query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value()))
|
||||
}
|
||||
|
||||
if options.PosterID.Has() {
|
||||
|
|
|
@ -105,7 +105,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
|
|||
NoLabel: len(labels) == 0,
|
||||
MilestoneID: issue.MilestoneID,
|
||||
ProjectID: projectID,
|
||||
ProjectBoardID: issue.ProjectBoardID(ctx),
|
||||
ProjectColumnID: issue.ProjectColumnID(ctx),
|
||||
PosterID: issue.PosterID,
|
||||
AssigneeID: issue.AssigneeID,
|
||||
MentionIDs: mentionIDs,
|
||||
|
|
|
@ -36,7 +36,7 @@ type Collector struct {
|
|||
Oauths *prometheus.Desc
|
||||
Organizations *prometheus.Desc
|
||||
Projects *prometheus.Desc
|
||||
ProjectBoards *prometheus.Desc
|
||||
ProjectColumns *prometheus.Desc
|
||||
PublicKeys *prometheus.Desc
|
||||
Releases *prometheus.Desc
|
||||
Repositories *prometheus.Desc
|
||||
|
@ -146,9 +146,9 @@ func NewCollector() Collector {
|
|||
"Number of projects",
|
||||
nil, nil,
|
||||
),
|
||||
ProjectBoards: prometheus.NewDesc(
|
||||
namespace+"projects_boards",
|
||||
"Number of project boards",
|
||||
ProjectColumns: prometheus.NewDesc(
|
||||
namespace+"projects_boards", // TODO: change the key name will affect the consume's result history
|
||||
"Number of project columns",
|
||||
nil, nil,
|
||||
),
|
||||
PublicKeys: prometheus.NewDesc(
|
||||
|
@ -219,7 +219,7 @@ func (c Collector) Describe(ch chan<- *prometheus.Desc) {
|
|||
ch <- c.Oauths
|
||||
ch <- c.Organizations
|
||||
ch <- c.Projects
|
||||
ch <- c.ProjectBoards
|
||||
ch <- c.ProjectColumns
|
||||
ch <- c.PublicKeys
|
||||
ch <- c.Releases
|
||||
ch <- c.Repositories
|
||||
|
@ -336,9 +336,9 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) {
|
|||
float64(stats.Counter.Project),
|
||||
)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.ProjectBoards,
|
||||
c.ProjectColumns,
|
||||
prometheus.GaugeValue,
|
||||
float64(stats.Counter.ProjectBoard),
|
||||
float64(stats.Counter.ProjectColumn),
|
||||
)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
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)
|
||||
|
||||
minioClient, err := minio.New(config.Endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(config.AccessKeyID, config.SecretAccessKey, ""),
|
||||
Creds: buildMinioCredentials(config, credentials.DefaultIAMRoleEndpoint),
|
||||
Secure: config.UseSSL,
|
||||
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}},
|
||||
Region: config.Location,
|
||||
|
@ -164,6 +164,35 @@ func (m *MinioStorage) buildMinioDirPrefix(p string) string {
|
|||
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
|
||||
func (m *MinioStorage) Open(path string) (Object, error) {
|
||||
opts := minio.GetObjectOptions{}
|
||||
|
|
|
@ -6,6 +6,7 @@ package storage
|
|||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
|
@ -109,3 +110,106 @@ func TestS3StorageBadRequest(t *testing.T) {
|
|||
_, err := NewStorage(setting.MinioStorageType, cfg)
|
||||
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
|
||||
MirrorUpdated time.Time `json:"mirror_updated,omitempty"`
|
||||
RepoTransfer *RepoTransfer `json:"repo_transfer"`
|
||||
Topics []string `json:"topics"`
|
||||
}
|
||||
|
||||
// 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
|
||||
issues = Issues
|
||||
pulls = Pull requests
|
||||
project_board = Projects
|
||||
project = Projects
|
||||
packages = Packages
|
||||
actions = Actions
|
||||
release = Release
|
||||
|
@ -1475,6 +1475,7 @@ issues.new.assignees = Assignees
|
|||
issues.new.clear_assignees = Clear assignees
|
||||
issues.new.no_assignees = No assignees
|
||||
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.open_external_link = Open
|
||||
issues.choose.blank = Default
|
||||
|
@ -1792,6 +1793,7 @@ compare.compare_head = compare
|
|||
pulls.desc = Enable pull requests and code reviews.
|
||||
pulls.new = New 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.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
|
||||
|
@ -1947,6 +1949,8 @@ pulls.recently_pushed_new_branches = You pushed on branch <a href="%[3]s"><stron
|
|||
|
||||
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.closed = Closed %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.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 = The workflow must contain at least one job
|
||||
runs.actor = Actor
|
||||
runs.status = Status
|
||||
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 {
|
||||
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 errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
|
||||
ctx.Error(http.StatusBadRequest, "ChangeContent", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -220,7 +220,7 @@ func CreateIssueAttachment(ctx *context.APIContext) {
|
|||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -586,7 +586,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
|
|||
|
||||
oldContent := comment.Content
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -225,7 +225,7 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
|
|||
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)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -116,23 +116,39 @@ func ListPullRequests(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -609,8 +625,13 @@ func EditPullRequest(ctx *context.APIContext) {
|
|||
}
|
||||
}
|
||||
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 errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
|
||||
ctx.Error(http.StatusBadRequest, "ChangeContent", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -94,6 +94,7 @@ func UnadoptedRepos(ctx *context.Context) {
|
|||
repoNames, count, err := repo_service.ListUnadoptedRepositories(ctx, q, &opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListUnadoptedRepositories", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Dirs"] = repoNames
|
||||
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 err := user_model.ActivateEmail(ctx, email); err != nil {
|
||||
ctx.ServerError("ActivateEmail", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Email activated: %s", email.Email)
|
||||
|
|
|
@ -34,7 +34,7 @@ const (
|
|||
// MustEnableProjects check if projects are enabled in settings
|
||||
func MustEnableProjects(ctx *context.Context) {
|
||||
if unit.TypeProjects.UnitGlobalDisabled() {
|
||||
ctx.NotFound("EnableKanbanBoard", nil)
|
||||
ctx.NotFound("EnableProjects", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ func MustEnableProjects(ctx *context.Context) {
|
|||
// Projects renders the home page of projects
|
||||
func Projects(ctx *context.Context) {
|
||||
shared_user.PrepareContextForProfileBigAvatar(ctx)
|
||||
ctx.Data["Title"] = ctx.Tr("repo.project_board")
|
||||
ctx.Data["Title"] = ctx.Tr("repo.projects")
|
||||
|
||||
sortType := ctx.FormTrim("sort")
|
||||
|
||||
|
@ -139,7 +139,7 @@ func canWriteProjects(ctx *context.Context) bool {
|
|||
// RenderNewProject render creating a project page
|
||||
func RenderNewProject(ctx *context.Context) {
|
||||
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["CanWriteProjects"] = canWriteProjects(ctx)
|
||||
ctx.Data["PageIsViewProjects"] = true
|
||||
|
@ -172,7 +172,7 @@ func NewProjectPost(ctx *context.Context) {
|
|||
Title: form.Title,
|
||||
Description: form.Content,
|
||||
CreatorID: ctx.Doer.ID,
|
||||
BoardType: form.BoardType,
|
||||
TemplateType: form.TemplateType,
|
||||
CardType: form.CardType,
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
|
@ -326,15 +326,15 @@ func ViewProject(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
boards, err := project.GetBoards(ctx)
|
||||
columns, err := project.GetColumns(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectBoards", err)
|
||||
ctx.ServerError("GetProjectColumns", err)
|
||||
return
|
||||
}
|
||||
|
||||
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
|
||||
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadIssuesOfBoards", err)
|
||||
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -377,7 +377,7 @@ func ViewProject(ctx *context.Context) {
|
|||
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
|
||||
ctx.Data["Project"] = project
|
||||
ctx.Data["IssuesMap"] = issuesMap
|
||||
ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend
|
||||
ctx.Data["Columns"] = columns
|
||||
shared_user.RenderUserHeader(ctx)
|
||||
|
||||
err = shared_user.LoadHeaderCount(ctx)
|
||||
|
@ -389,8 +389,8 @@ func ViewProject(ctx *context.Context) {
|
|||
ctx.HTML(http.StatusOK, tplProjectsView)
|
||||
}
|
||||
|
||||
// DeleteProjectBoard allows for the deletion of a project board
|
||||
func DeleteProjectBoard(ctx *context.Context) {
|
||||
// DeleteProjectColumn allows for the deletion of a project column
|
||||
func DeleteProjectColumn(ctx *context.Context) {
|
||||
if ctx.Doer == nil {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "Only signed in users are allowed to perform this action.",
|
||||
|
@ -404,36 +404,36 @@ func DeleteProjectBoard(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
|
||||
pb, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectBoard", err)
|
||||
ctx.ServerError("GetProjectColumn", err)
|
||||
return
|
||||
}
|
||||
if pb.ProjectID != ctx.ParamsInt64(":id") {
|
||||
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
|
||||
}
|
||||
|
||||
if project.OwnerID != ctx.ContextUser.ID {
|
||||
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
|
||||
}
|
||||
|
||||
if err := project_model.DeleteBoardByID(ctx, ctx.ParamsInt64(":boardID")); err != nil {
|
||||
ctx.ServerError("DeleteProjectBoardByID", err)
|
||||
if err := project_model.DeleteColumnByID(ctx, ctx.ParamsInt64(":columnID")); err != nil {
|
||||
ctx.ServerError("DeleteProjectColumnByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// AddBoardToProjectPost allows a new board to be added to a project.
|
||||
func AddBoardToProjectPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
|
||||
// AddColumnToProjectPost allows a new column to be added to a project.
|
||||
func AddColumnToProjectPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
|
||||
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
|
@ -441,21 +441,21 @@ func AddBoardToProjectPost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := project_model.NewBoard(ctx, &project_model.Board{
|
||||
if err := project_model.NewColumn(ctx, &project_model.Column{
|
||||
ProjectID: project.ID,
|
||||
Title: form.Title,
|
||||
Color: form.Color,
|
||||
CreatorID: ctx.Doer.ID,
|
||||
}); err != nil {
|
||||
ctx.ServerError("NewProjectBoard", err)
|
||||
ctx.ServerError("NewProjectColumn", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// CheckProjectBoardChangePermissions check permission
|
||||
func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Board) {
|
||||
// CheckProjectColumnChangePermissions check permission
|
||||
func CheckProjectColumnChangePermissions(ctx *context.Context) (*project_model.Project, *project_model.Column) {
|
||||
if ctx.Doer == nil {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"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
|
||||
}
|
||||
|
||||
board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
|
||||
column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectBoard", err)
|
||||
ctx.ServerError("GetProjectColumn", err)
|
||||
return nil, nil
|
||||
}
|
||||
if board.ProjectID != ctx.ParamsInt64(":id") {
|
||||
if column.ProjectID != ctx.ParamsInt64(":id") {
|
||||
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
|
||||
}
|
||||
|
||||
if project.OwnerID != ctx.ContextUser.ID {
|
||||
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 project, board
|
||||
return project, column
|
||||
}
|
||||
|
||||
// EditProjectBoard allows a project board's to be updated
|
||||
func EditProjectBoard(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
|
||||
_, board := CheckProjectBoardChangePermissions(ctx)
|
||||
// EditProjectColumn allows a project column's to be updated
|
||||
func EditProjectColumn(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
|
||||
_, column := CheckProjectColumnChangePermissions(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if form.Title != "" {
|
||||
board.Title = form.Title
|
||||
column.Title = form.Title
|
||||
}
|
||||
|
||||
board.Color = form.Color
|
||||
|
||||
column.Color = form.Color
|
||||
if form.Sorting != 0 {
|
||||
board.Sorting = form.Sorting
|
||||
column.Sorting = form.Sorting
|
||||
}
|
||||
|
||||
if err := project_model.UpdateBoard(ctx, board); err != nil {
|
||||
ctx.ServerError("UpdateProjectBoard", err)
|
||||
if err := project_model.UpdateColumn(ctx, column); err != nil {
|
||||
ctx.ServerError("UpdateProjectColumn", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// SetDefaultProjectBoard set default board for uncategorized issues/pulls
|
||||
func SetDefaultProjectBoard(ctx *context.Context) {
|
||||
project, board := CheckProjectBoardChangePermissions(ctx)
|
||||
// SetDefaultProjectColumn set default column for uncategorized issues/pulls
|
||||
func SetDefaultProjectColumn(ctx *context.Context) {
|
||||
project, column := CheckProjectColumnChangePermissions(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_model.SetDefaultBoard(ctx, project.ID, board.ID); err != nil {
|
||||
ctx.ServerError("SetDefaultBoard", err)
|
||||
if err := project_model.SetDefaultColumn(ctx, project.ID, column.ID); err != nil {
|
||||
ctx.ServerError("SetDefaultColumn", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -550,14 +548,14 @@ func MoveIssues(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
|
||||
column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectBoard", project_model.IsErrProjectBoardNotExist, err)
|
||||
ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
if board.ProjectID != project.ID {
|
||||
ctx.NotFound("BoardNotInProject", nil)
|
||||
if column.ProjectID != project.ID {
|
||||
ctx.NotFound("ColumnNotInProject", nil)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -571,6 +569,7 @@ func MoveIssues(ctx *context.Context) {
|
|||
form := &movedIssuesForm{}
|
||||
if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
|
||||
ctx.ServerError("DecodeMovedIssuesForm", err)
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
ctx.ServerError("MoveIssuesOnProjectBoard", err)
|
||||
if err = project_model.MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs); err != nil {
|
||||
ctx.ServerError("MoveIssuesOnProjectColumn", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -13,16 +13,16 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheckProjectBoardChangePermissions(t *testing.T) {
|
||||
func TestCheckProjectColumnChangePermissions(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "user2/-/projects/4/4")
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
ctx.ContextUser = ctx.Doer // user2
|
||||
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, board)
|
||||
assert.NotNil(t, column)
|
||||
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.
|
||||
hasJobWithoutNeeds := false
|
||||
// Check whether have matching runner and a job without "needs"
|
||||
emptyJobsNumber := 0
|
||||
for _, j := range wf.Jobs {
|
||||
if j == nil {
|
||||
emptyJobsNumber++
|
||||
continue
|
||||
}
|
||||
if !hasJobWithoutNeeds && len(j.Needs()) == 0 {
|
||||
hasJobWithoutNeeds = true
|
||||
}
|
||||
|
@ -131,6 +136,9 @@ func List(ctx *context.Context) {
|
|||
if !hasJobWithoutNeeds {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -604,6 +604,7 @@ func DeleteFilePost(ctx *context.Context) {
|
|||
} else {
|
||||
ctx.ServerError("DeleteRepoFile", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
|
||||
|
|
|
@ -2239,8 +2239,16 @@ func UpdateIssueContent(ctx *context.Context) {
|
|||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -2267,6 +2275,7 @@ func UpdateIssueContent(ctx *context.Context) {
|
|||
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"content": content,
|
||||
"contentVersion": issue.ContentVersion,
|
||||
"attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content),
|
||||
})
|
||||
}
|
||||
|
@ -2826,7 +2835,7 @@ func ListIssues(ctx *context.Context) {
|
|||
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||
IsPull: isPull,
|
||||
IsClosed: isClosed,
|
||||
ProjectBoardID: projectID,
|
||||
ProjectID: projectID,
|
||||
SortBy: issue_indexer.SortByCreatedDesc,
|
||||
}
|
||||
if since != 0 {
|
||||
|
@ -3155,9 +3164,16 @@ func UpdateCommentContent(ctx *context.Context) {
|
|||
}
|
||||
|
||||
oldContent := comment.Content
|
||||
comment.Content = ctx.FormString("content")
|
||||
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
|
||||
newContent := ctx.FormString("content")
|
||||
contentVersion := ctx.FormInt("content_version")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -3189,6 +3205,7 @@ func UpdateCommentContent(ctx *context.Context) {
|
|||
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"content": content,
|
||||
"contentVersion": comment.ContentVersion,
|
||||
"attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ const (
|
|||
// MustEnableProjects check if projects are enabled in settings
|
||||
func MustEnableProjects(ctx *context.Context) {
|
||||
if unit.TypeProjects.UnitGlobalDisabled() {
|
||||
ctx.NotFound("EnableKanbanBoard", nil)
|
||||
ctx.NotFound("EnableRepoProjects", nil)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,7 @@ func MustEnableProjects(ctx *context.Context) {
|
|||
|
||||
// Projects renders the home page of projects
|
||||
func Projects(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.project_board")
|
||||
ctx.Data["Title"] = ctx.Tr("repo.projects")
|
||||
|
||||
sortType := ctx.FormTrim("sort")
|
||||
|
||||
|
@ -131,7 +131,7 @@ func Projects(ctx *context.Context) {
|
|||
// RenderNewProject render creating a project page
|
||||
func RenderNewProject(ctx *context.Context) {
|
||||
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["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
|
||||
ctx.Data["CancelLink"] = ctx.Repo.Repository.Link() + "/projects"
|
||||
|
@ -153,7 +153,7 @@ func NewProjectPost(ctx *context.Context) {
|
|||
Title: form.Title,
|
||||
Description: form.Content,
|
||||
CreatorID: ctx.Doer.ID,
|
||||
BoardType: form.BoardType,
|
||||
TemplateType: form.TemplateType,
|
||||
CardType: form.CardType,
|
||||
Type: project_model.TypeRepository,
|
||||
}); err != nil {
|
||||
|
@ -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) {
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
|
@ -304,15 +304,15 @@ func ViewProject(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
boards, err := project.GetBoards(ctx)
|
||||
columns, err := project.GetColumns(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectBoards", err)
|
||||
ctx.ServerError("GetProjectColumns", err)
|
||||
return
|
||||
}
|
||||
|
||||
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
|
||||
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadIssuesOfBoards", err)
|
||||
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -367,7 +367,7 @@ func ViewProject(ctx *context.Context) {
|
|||
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
|
||||
ctx.Data["Project"] = project
|
||||
ctx.Data["IssuesMap"] = issuesMap
|
||||
ctx.Data["Columns"] = boards // TODO: rename boards to columns in backend
|
||||
ctx.Data["Columns"] = columns
|
||||
|
||||
ctx.HTML(http.StatusOK, tplProjectsView)
|
||||
}
|
||||
|
@ -405,8 +405,8 @@ func UpdateIssueProject(ctx *context.Context) {
|
|||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// DeleteProjectBoard allows for the deletion of a project board
|
||||
func DeleteProjectBoard(ctx *context.Context) {
|
||||
// DeleteProjectColumn allows for the deletion of a project column
|
||||
func DeleteProjectColumn(ctx *context.Context) {
|
||||
if ctx.Doer == nil {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "Only signed in users are allowed to perform this action.",
|
||||
|
@ -431,36 +431,36 @@ func DeleteProjectBoard(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
|
||||
pb, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectBoard", err)
|
||||
ctx.ServerError("GetProjectColumn", err)
|
||||
return
|
||||
}
|
||||
if pb.ProjectID != ctx.ParamsInt64(":id") {
|
||||
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
|
||||
}
|
||||
|
||||
if project.RepoID != ctx.Repo.Repository.ID {
|
||||
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
|
||||
}
|
||||
|
||||
if err := project_model.DeleteBoardByID(ctx, ctx.ParamsInt64(":boardID")); err != nil {
|
||||
ctx.ServerError("DeleteProjectBoardByID", err)
|
||||
if err := project_model.DeleteColumnByID(ctx, ctx.ParamsInt64(":columnID")); err != nil {
|
||||
ctx.ServerError("DeleteProjectColumnByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// AddBoardToProjectPost allows a new board to be added to a project.
|
||||
func AddBoardToProjectPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
|
||||
// AddColumnToProjectPost allows a new column to be added to a project.
|
||||
func AddColumnToProjectPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
|
||||
if !ctx.Repo.IsOwner() && !ctx.Repo.IsAdmin() && !ctx.Repo.CanAccess(perm.AccessModeWrite, unit.TypeProjects) {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "Only authorized users are allowed to perform this action.",
|
||||
|
@ -478,20 +478,20 @@ func AddBoardToProjectPost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := project_model.NewBoard(ctx, &project_model.Board{
|
||||
if err := project_model.NewColumn(ctx, &project_model.Column{
|
||||
ProjectID: project.ID,
|
||||
Title: form.Title,
|
||||
Color: form.Color,
|
||||
CreatorID: ctx.Doer.ID,
|
||||
}); err != nil {
|
||||
ctx.ServerError("NewProjectBoard", err)
|
||||
ctx.ServerError("NewProjectColumn", err)
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"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
|
||||
}
|
||||
|
||||
board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
|
||||
column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectBoard", err)
|
||||
ctx.ServerError("GetProjectColumn", err)
|
||||
return nil, nil
|
||||
}
|
||||
if board.ProjectID != ctx.ParamsInt64(":id") {
|
||||
if column.ProjectID != ctx.ParamsInt64(":id") {
|
||||
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
|
||||
}
|
||||
|
||||
if project.RepoID != ctx.Repo.Repository.ID {
|
||||
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 project, board
|
||||
return project, column
|
||||
}
|
||||
|
||||
// EditProjectBoard allows a project board's to be updated
|
||||
func EditProjectBoard(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.EditProjectBoardForm)
|
||||
_, board := checkProjectBoardChangePermissions(ctx)
|
||||
// EditProjectColumn allows a project column's to be updated
|
||||
func EditProjectColumn(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
|
||||
_, column := checkProjectColumnChangePermissions(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if form.Title != "" {
|
||||
board.Title = form.Title
|
||||
column.Title = form.Title
|
||||
}
|
||||
|
||||
board.Color = form.Color
|
||||
|
||||
column.Color = form.Color
|
||||
if form.Sorting != 0 {
|
||||
board.Sorting = form.Sorting
|
||||
column.Sorting = form.Sorting
|
||||
}
|
||||
|
||||
if err := project_model.UpdateBoard(ctx, board); err != nil {
|
||||
ctx.ServerError("UpdateProjectBoard", err)
|
||||
if err := project_model.UpdateColumn(ctx, column); err != nil {
|
||||
ctx.ServerError("UpdateProjectColumn", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// SetDefaultProjectBoard set default board for uncategorized issues/pulls
|
||||
func SetDefaultProjectBoard(ctx *context.Context) {
|
||||
project, board := checkProjectBoardChangePermissions(ctx)
|
||||
// SetDefaultProjectColumn set default column for uncategorized issues/pulls
|
||||
func SetDefaultProjectColumn(ctx *context.Context) {
|
||||
project, column := checkProjectColumnChangePermissions(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_model.SetDefaultBoard(ctx, project.ID, board.ID); err != nil {
|
||||
ctx.ServerError("SetDefaultBoard", err)
|
||||
if err := project_model.SetDefaultColumn(ctx, project.ID, column.ID); err != nil {
|
||||
ctx.ServerError("SetDefaultColumn", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -608,18 +606,18 @@ func MoveIssues(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
|
||||
column, err := project_model.GetColumn(ctx, ctx.ParamsInt64(":columnID"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectBoardNotExist(err) {
|
||||
ctx.NotFound("ProjectBoardNotExist", nil)
|
||||
if project_model.IsErrProjectColumnNotExist(err) {
|
||||
ctx.NotFound("ProjectColumnNotExist", nil)
|
||||
} else {
|
||||
ctx.ServerError("GetProjectBoard", err)
|
||||
ctx.ServerError("GetProjectColumn", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if board.ProjectID != project.ID {
|
||||
ctx.NotFound("BoardNotInProject", nil)
|
||||
if column.ProjectID != project.ID {
|
||||
ctx.NotFound("ColumnNotInProject", nil)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -663,8 +661,8 @@ func MoveIssues(ctx *context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
if err = project_model.MoveIssuesOnProjectBoard(ctx, board, sortedIssueIDs); err != nil {
|
||||
ctx.ServerError("MoveIssuesOnProjectBoard", err)
|
||||
if err = project_model.MoveIssuesOnProjectColumn(ctx, column, sortedIssueIDs); err != nil {
|
||||
ctx.ServerError("MoveIssuesOnProjectColumn", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -12,16 +12,16 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheckProjectBoardChangePermissions(t *testing.T) {
|
||||
func TestCheckProjectColumnChangePermissions(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
ctx, _ := contexttest.MockContext(t, "user2/repo1/projects/1/2")
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
contexttest.LoadRepo(t, ctx, 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, board)
|
||||
assert.NotNil(t, column)
|
||||
assert.False(t, ctx.Written())
|
||||
}
|
||||
|
|
|
@ -80,7 +80,6 @@ func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int
|
|||
PageSize: 30,
|
||||
},
|
||||
Status: actions_model.StatusUnknown, // Unknown means all
|
||||
IDOrderDesc: true,
|
||||
RunnerID: runner.ID,
|
||||
}
|
||||
|
||||
|
|
|
@ -978,7 +978,7 @@ func registerRoutes(m *web.Route) {
|
|||
m.Get("/new", org.RenderNewProject)
|
||||
m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
|
||||
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("/delete", org.DeleteProject)
|
||||
|
||||
|
@ -986,10 +986,10 @@ func registerRoutes(m *web.Route) {
|
|||
m.Post("/edit", web.Bind(forms.CreateProjectForm{}), org.EditProjectPost)
|
||||
m.Post("/{action:open|close}", org.ChangeProjectStatus)
|
||||
|
||||
m.Group("/{boardID}", func() {
|
||||
m.Put("", web.Bind(forms.EditProjectBoardForm{}), org.EditProjectBoard)
|
||||
m.Delete("", org.DeleteProjectBoard)
|
||||
m.Post("/default", org.SetDefaultProjectBoard)
|
||||
m.Group("/{columnID}", func() {
|
||||
m.Put("", web.Bind(forms.EditProjectColumnForm{}), org.EditProjectColumn)
|
||||
m.Delete("", org.DeleteProjectColumn)
|
||||
m.Post("/default", org.SetDefaultProjectColumn)
|
||||
|
||||
m.Post("/move", org.MoveIssues)
|
||||
})
|
||||
|
@ -1352,7 +1352,7 @@ func registerRoutes(m *web.Route) {
|
|||
m.Get("/new", repo.RenderNewProject)
|
||||
m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
|
||||
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("/delete", repo.DeleteProject)
|
||||
|
||||
|
@ -1360,10 +1360,10 @@ func registerRoutes(m *web.Route) {
|
|||
m.Post("/edit", web.Bind(forms.CreateProjectForm{}), repo.EditProjectPost)
|
||||
m.Post("/{action:open|close}", repo.ChangeProjectStatus)
|
||||
|
||||
m.Group("/{boardID}", func() {
|
||||
m.Put("", web.Bind(forms.EditProjectBoardForm{}), repo.EditProjectBoard)
|
||||
m.Delete("", repo.DeleteProjectBoard)
|
||||
m.Post("/default", repo.SetDefaultProjectBoard)
|
||||
m.Group("/{columnID}", func() {
|
||||
m.Put("", web.Bind(forms.EditProjectColumnForm{}), repo.EditProjectColumn)
|
||||
m.Delete("", repo.DeleteProjectColumn)
|
||||
m.Post("/default", repo.SetDefaultProjectColumn)
|
||||
|
||||
m.Post("/move", repo.MoveIssues)
|
||||
})
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
@ -141,18 +140,19 @@ func (r *jobStatusResolver) resolve() map[int64]actions_model.Status {
|
|||
if allSucceed {
|
||||
ret[id] = actions_model.StatusWaiting
|
||||
} else {
|
||||
// If a job's "if" condition is "always()", the job should always run even if some of its dependencies did not succeed.
|
||||
// See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds
|
||||
always := false
|
||||
// Check if the job has an "if" condition
|
||||
hasIf := false
|
||||
if wfJobs, _ := jobparser.Parse(r.jobMap[id].WorkflowPayload); len(wfJobs) == 1 {
|
||||
_, wfJob := wfJobs[0].Job()
|
||||
expr := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(wfJob.If.Value, "${{"), "}}"))
|
||||
always = expr == "always()"
|
||||
hasIf = len(wfJob.If.Value) > 0
|
||||
}
|
||||
|
||||
if always {
|
||||
if hasIf {
|
||||
// act_runner will check the "if" condition
|
||||
ret[id] = actions_model.StatusWaiting
|
||||
} else {
|
||||
// If the "if" condition is empty and not all dependent jobs completed successfully,
|
||||
// the job should be skipped.
|
||||
ret[id] = actions_model.StatusSkipped
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,9 +71,9 @@ func Test_jobStatusResolver_Resolve(t *testing.T) {
|
|||
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{
|
||||
{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(
|
||||
`
|
||||
name: test
|
||||
|
@ -82,15 +82,15 @@ jobs:
|
|||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: job1
|
||||
if: ${{ always() }}
|
||||
if: ${{ always() && needs.job1.result == 'success' }}
|
||||
steps:
|
||||
- run: echo "always run"
|
||||
- run: echo "will be checked by act_runner"
|
||||
`)},
|
||||
},
|
||||
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{
|
||||
{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
|
||||
{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
||||
|
@ -101,15 +101,15 @@ jobs:
|
|||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: job1
|
||||
if: always()
|
||||
if: ${{ always() && needs.job1.result == 'failure' }}
|
||||
steps:
|
||||
- run: echo "always run"
|
||||
- run: echo "will be checked by act_runner"
|
||||
`)},
|
||||
},
|
||||
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{
|
||||
{ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
|
||||
{ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
|
||||
|
@ -121,7 +121,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
needs: job1
|
||||
steps:
|
||||
- run: echo "not always run"
|
||||
- run: echo "should be skipped"
|
||||
`)},
|
||||
},
|
||||
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 {
|
||||
if err := issue.LoadLabels(ctx); err != nil {
|
||||
return &api.Issue{}
|
||||
}
|
||||
if err := issue.LoadPoster(ctx); err != nil {
|
||||
return &api.Issue{}
|
||||
}
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return &api.Issue{}
|
||||
}
|
||||
if err := issue.LoadAttachments(ctx); err != nil {
|
||||
return &api.Issue{}
|
||||
}
|
||||
|
||||
apiIssue := &api.Issue{
|
||||
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.HTMLURL = issue.HTMLURL()
|
||||
if err := issue.LoadLabels(ctx); err != nil {
|
||||
return &api.Issue{}
|
||||
}
|
||||
apiIssue.Labels = ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner)
|
||||
apiIssue.Repo = &api.RepositoryMeta{
|
||||
ID: issue.Repo.ID,
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
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/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
@ -44,7 +45,16 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
|
|||
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 {
|
||||
log.Error("GetUserRepoPermission[%d]: %v", pr.BaseRepoID, err)
|
||||
p.AccessMode = perm.AccessModeNone
|
||||
|
|
|
@ -237,6 +237,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
|||
MirrorInterval: mirrorInterval,
|
||||
MirrorUpdated: mirrorUpdated,
|
||||
RepoTransfer: transfer,
|
||||
Topics: repo.Topics,
|
||||
ObjectFormatName: repo.ObjectFormatName,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -370,45 +370,21 @@ func (i IssueLockForm) HasValidReason() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// __________ __ __
|
||||
// \______ \_______ ____ |__| ____ _____/ |_ ______
|
||||
// | ___/\_ __ \/ _ \ | |/ __ \_/ ___\ __\/ ___/
|
||||
// | | | | \( <_> ) | \ ___/\ \___| | \___ \
|
||||
// |____| |__| \____/\__| |\___ >\___ >__| /____ >
|
||||
// \______| \/ \/ \/
|
||||
|
||||
// CreateProjectForm form for creating a project
|
||||
type CreateProjectForm struct {
|
||||
Title string `binding:"Required;MaxSize(100)"`
|
||||
Content string
|
||||
BoardType project_model.BoardType
|
||||
TemplateType project_model.TemplateType
|
||||
CardType project_model.CardType
|
||||
}
|
||||
|
||||
// UserCreateProjectForm is a from for creating an individual or organization
|
||||
// form.
|
||||
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 {
|
||||
// EditProjectColumnForm is a form for editing a project column
|
||||
type EditProjectColumnForm struct {
|
||||
Title string `binding:"Required;MaxSize(100)"`
|
||||
Sorting int8
|
||||
Color string `binding:"MaxSize(7)"`
|
||||
}
|
||||
|
||||
// _____ .__.__ __
|
||||
// / \ |__| | ____ _______/ |_ ____ ____ ____
|
||||
// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
|
||||
// / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/
|
||||
// \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ >
|
||||
// \/ \/ \/ \/ \/
|
||||
|
||||
// CreateMilestoneForm form for creating milestone
|
||||
type CreateMilestoneForm struct {
|
||||
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)
|
||||
}
|
||||
|
||||
// .____ ___. .__
|
||||
// | | _____ \_ |__ ____ | |
|
||||
// | | \__ \ | __ \_/ __ \| |
|
||||
// | |___ / __ \| \_\ \ ___/| |__
|
||||
// |_______ (____ /___ /\___ >____/
|
||||
// \/ \/ \/ \/
|
||||
|
||||
// CreateLabelForm form for creating label
|
||||
type CreateLabelForm struct {
|
||||
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)
|
||||
}
|
||||
|
||||
// __________ .__ .__ __________ __
|
||||
// \______ \__ __| | | | \______ \ ____ ________ __ ____ _______/ |_
|
||||
// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\
|
||||
// | | | | / |_| |__ | | \ ___< <_| | | /\ ___/ \___ \ | |
|
||||
// |____| |____/|____/____/ |____|_ /\___ >__ |____/ \___ >____ > |__|
|
||||
// \/ \/ |__| \/ \/
|
||||
|
||||
// MergePullRequestForm form for merging Pull Request
|
||||
// swagger:model MergePullRequestOption
|
||||
type MergePullRequestForm struct {
|
||||
|
|
|
@ -65,7 +65,7 @@ var hiddenCommentTypeGroups = hiddenCommentTypeGroupsType{
|
|||
},
|
||||
"project": {
|
||||
/*30*/ issues_model.CommentTypeProject,
|
||||
/*31*/ issues_model.CommentTypeProjectBoard,
|
||||
/*31*/ issues_model.CommentTypeProjectColumn,
|
||||
},
|
||||
"issue_ref": {
|
||||
/*33*/ issues_model.CommentTypeChangeIssueRef,
|
||||
|
|
|
@ -39,7 +39,8 @@ func TestDeleteNotPassedAssignee(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
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.Empty(t, issue.Assignees)
|
||||
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.
|
||||
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()
|
||||
if needsContentHistory {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -12,10 +12,10 @@ import (
|
|||
)
|
||||
|
||||
// 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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
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
|
||||
|
|
|
@ -25,11 +25,11 @@
|
|||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.projects.template.desc"}}</label>
|
||||
<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="menu">
|
||||
{{range $element := .BoardTypes}}
|
||||
<div class="item" data-id="{{$element.BoardType}}" data-value="{{$element.BoardType}}">{{ctx.Locale.Tr $element.Translation}}</div>
|
||||
{{range $element := .TemplateConfigs}}
|
||||
<div class="item" data-id="{{$element.TemplateType}}" data-value="{{$element.TemplateType}}">{{ctx.Locale.Tr $element.Translation}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
{{end}}
|
||||
</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}}
|
||||
{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
|
||||
{{end}}
|
||||
|
|
|
@ -131,7 +131,7 @@
|
|||
|
||||
{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects)}}
|
||||
<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}}
|
||||
<span class="ui small label">{{CountFmt .Repository.NumOpenProjects}}</span>
|
||||
{{end}}
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
<!-- Projects -->
|
||||
<div class="ui{{if not (or .OpenProjects .ClosedProjects)}} disabled{{end}} dropdown jump item">
|
||||
<span class="text">
|
||||
{{ctx.Locale.Tr "repo.project_board"}}
|
||||
{{ctx.Locale.Tr "repo.projects"}}
|
||||
</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
{{end}}
|
||||
</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}}
|
||||
{{template "repo/issue/view_content/attachments" dict "Attachments" .Issue.Attachments "RenderedContent" .Issue.RenderedContent}}
|
||||
{{end}}
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
{{end}}
|
||||
</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}}
|
||||
{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
|
||||
{{end}}
|
||||
|
@ -441,7 +441,7 @@
|
|||
{{end}}
|
||||
</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}}
|
||||
{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
|
||||
{{end}}
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
{{end}}
|
||||
</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}}
|
||||
{{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}}
|
||||
{{end}}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
{{$isProjectsEnabled := .Repository.UnitEnabled $.Context $.UnitTypeProjects}}
|
||||
{{$isProjectsGlobalDisabled := .UnitTypeProjects.UnitGlobalDisabled}}
|
||||
<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}}>
|
||||
<input class="enable-system" name="enable_projects" type="checkbox" {{if $isProjectsEnabled}}checked{{end}}>
|
||||
<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",
|
||||
"x-go-name": "Template"
|
||||
},
|
||||
"topics": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"x-go-name": "Topics"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"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) {
|
||||
return func(t *testing.T) {
|
||||
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) {
|
||||
runTestGitPush(t, u, objectFormat, func(t *testing.T, gitPath string) (pushed, deleted []string) {
|
||||
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) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user2")
|
||||
|
@ -401,6 +429,7 @@ func TestIssueCommentUpdate(t *testing.T) {
|
|||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
|
||||
"_csrf": GetCSRF(t, session, issueURL),
|
||||
"content": "",
|
||||
"content_version": fmt.Sprintf("%d", comment.ContentVersion),
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
|
@ -408,6 +437,44 @@ func TestIssueCommentUpdate(t *testing.T) {
|
|||
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) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user2")
|
||||
|
|
|
@ -38,20 +38,20 @@ func TestMoveRepoProjectColumns(t *testing.T) {
|
|||
Title: "new created project",
|
||||
RepoID: repo2.ID,
|
||||
Type: project_model.TypeRepository,
|
||||
BoardType: project_model.BoardTypeNone,
|
||||
TemplateType: project_model.TemplateTypeNone,
|
||||
}
|
||||
err := project_model.NewProject(db.DefaultContext, &project1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
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),
|
||||
ProjectID: project1.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
columns, err := project1.GetBoards(db.DefaultContext)
|
||||
columns, err := project1.GetColumns(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columns, 3)
|
||||
assert.EqualValues(t, 0, columns[0].Sorting)
|
||||
|
@ -72,7 +72,7 @@ func TestMoveRepoProjectColumns(t *testing.T) {
|
|||
})
|
||||
sess.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
columnsAfter, err := project1.GetBoards(db.DefaultContext)
|
||||
columnsAfter, err := project1.GetColumns(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, columns, 3)
|
||||
assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID)
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
}
|
||||
|
||||
.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;
|
||||
margin: 0 0.5rem !important;
|
||||
padding: 0.5rem !important;
|
||||
|
|
|
@ -218,7 +218,7 @@
|
|||
--color-expand-button: #2b353e;
|
||||
--color-placeholder-text: var(--color-text-light-3);
|
||||
--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-reaction-bg: #e8e8ff12;
|
||||
--color-reaction-hover-bg: var(--color-primary-light-4);
|
||||
|
|
|
@ -218,7 +218,7 @@
|
|||
--color-expand-button: #cfe8fa;
|
||||
--color-placeholder-text: var(--color-text-light-3);
|
||||
--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-reaction-bg: #0000170a;
|
||||
--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 {initRepoDiffConversationNav} from './repo-diff.js';
|
||||
import {createDropzone} from './dropzone.js';
|
||||
import {showErrorToast} from '../modules/toast.js';
|
||||
import {initCommentContent, initMarkupContent} from '../markup/content.js';
|
||||
import {initCompReactionSelector} from './comp/ReactionSelector.js';
|
||||
import {initRepoSettingBranches} from './repo-settings.js';
|
||||
|
@ -431,11 +432,17 @@ async function onEditContent(event) {
|
|||
const params = new URLSearchParams({
|
||||
content: comboMarkdownEditor.value(),
|
||||
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);
|
||||
|
||||
const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params});
|
||||
const data = await response.json();
|
||||
if (response.status === 400) {
|
||||
showErrorToast(data.errorMessage);
|
||||
return;
|
||||
}
|
||||
editContentZone.setAttribute('data-content-version', data.contentVersion);
|
||||
if (!data.content) {
|
||||
renderContent.innerHTML = document.getElementById('no-content').innerHTML;
|
||||
rawContent.textContent = '';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {POST} from '../modules/fetch.js';
|
||||
import {showErrorToast} from '../modules/toast.js';
|
||||
|
||||
const preventListener = (e) => e.preventDefault();
|
||||
|
||||
|
@ -54,13 +55,20 @@ export function initMarkupTasklist() {
|
|||
const editContentZone = container.querySelector('.edit-content-zone');
|
||||
const updateUrl = editContentZone.getAttribute('data-update-url');
|
||||
const context = editContentZone.getAttribute('data-context');
|
||||
const contentVersion = editContentZone.getAttribute('data-content-version');
|
||||
|
||||
const requestBody = new FormData();
|
||||
requestBody.append('ignore_attachments', 'true');
|
||||
requestBody.append('content', newContent);
|
||||
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;
|
||||
} catch (err) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
|
|
Loading…
Reference in a new issue