diff --git a/Gopkg.lock b/Gopkg.lock index 6551354a0..843827b5d 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -8,10 +8,11 @@ revision = "31f4b8e8c805438ac6d8914b38accb1d8aaf695e" [[projects]] - branch = "master" + branch = "migration" name = "code.gitea.io/sdk" packages = ["gitea"] - revision = "b2308e3f700875a3642a78bd3f6e5db8ef6f974d" + revision = "c01e6df2e1cdb53403f6542e5d2248ace831f3ec" + source = "github.com/JonasFranzDEV/go-sdk" [[projects]] name = "github.com/PuerkitoBio/goquery" @@ -873,6 +874,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "036b8c882671cf8d2c5e2fdbe53b1bdfbd39f7ebd7765bd50276c7c4ecf16687" + inputs-digest = "6e57d03c2ae6e7ee38ab336d3a8c2171b21f10f2b3f58ce7d19904cc725a2a06" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 1019888c0..b73e2b11a 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -11,7 +11,8 @@ ignored = ["google.golang.org/appengine*"] name = "code.gitea.io/git" [[constraint]] - branch = "master" + branch = "migration" + source = "github.com/JonasFranzDEV/go-sdk" name = "code.gitea.io/sdk" [[constraint]] diff --git a/models/issue.go b/models/issue.go index d97266b4e..10ff36165 100644 --- a/models/issue.go +++ b/models/issue.go @@ -30,12 +30,13 @@ type Issue struct { Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository. PosterID int64 `xorm:"INDEX"` Poster *User `xorm:"-"` - Title string `xorm:"name"` - Content string `xorm:"TEXT"` - RenderedContent string `xorm:"-"` - Labels []*Label `xorm:"-"` - MilestoneID int64 `xorm:"INDEX"` - Milestone *Milestone `xorm:"-"` + GhostName string + Title string `xorm:"name"` + Content string `xorm:"TEXT"` + RenderedContent string `xorm:"-"` + Labels []*Label `xorm:"-"` + MilestoneID int64 `xorm:"INDEX"` + Milestone *Milestone `xorm:"-"` Priority int AssigneeID int64 `xorm:"-"` Assignee *User `xorm:"-"` @@ -48,7 +49,7 @@ type Issue struct { DeadlineUnix util.TimeStamp `xorm:"INDEX"` - CreatedUnix util.TimeStamp `xorm:"INDEX created"` + CreatedUnix util.TimeStamp `xorm:"INDEX"` UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` ClosedUnix util.TimeStamp `xorm:"INDEX"` @@ -67,6 +68,13 @@ var ( const issueTasksRegexpStr = `(^\s*[-*]\s\[[\sx]\]\s.)|(\n\s*[-*]\s\[[\sx]\]\s.)` const issueTasksDoneRegexpStr = `(^\s*[-*]\s\[[x]\]\s.)|(\n\s*[-*]\s\[[x]\]\s.)` +// BeforeInsert is invoked before XORM inserts this +func (issue *Issue) BeforeInsert() { + if issue.CreatedUnix == util.TimeStamp(0) { + issue.CreatedUnix = util.TimeStampNow() + } +} + func init() { issueTasksPat = regexp.MustCompile(issueTasksRegexpStr) issueTasksDonePat = regexp.MustCompile(issueTasksDoneRegexpStr) @@ -134,6 +142,10 @@ func (issue *Issue) loadPoster(e Engine) (err error) { if !IsErrUserNotExist(err) { return fmt.Errorf("getUserByID.(poster) [%d]: %v", issue.PosterID, err) } + if issue.GhostName != "" { + issue.Poster.Name = issue.GhostName + issue.Poster.LowerName = strings.ToLower(issue.GhostName) + } err = nil return } @@ -892,7 +904,9 @@ type NewIssueOptions struct { func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) { opts.Issue.Title = strings.TrimSpace(opts.Issue.Title) - opts.Issue.Index = opts.Repo.NextIssueIndex() + if opts.Issue.Index == 0 { + opts.Issue.Index = opts.Repo.NextIssueIndex() + } if opts.Issue.MilestoneID > 0 { milestone, err := getMilestoneByRepoID(e, opts.Issue.RepoID, opts.Issue.MilestoneID) @@ -1010,15 +1024,11 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) { return opts.Issue.loadAttributes(e) } -// NewIssue creates new issue with labels for repository. -func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []int64, uuids []string) (err error) { +// NewSuppressedIssue creates an issue without sending notifications or webhooks +func NewSuppressedIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []int64, uuids []string) error { sess := x.NewSession() defer sess.Close() - if err = sess.Begin(); err != nil { - return err - } - - if err = newIssue(sess, issue.Poster, NewIssueOptions{ + if err := newIssue(sess, issue.Poster, NewIssueOptions{ Repo: repo, Issue: issue, LabelIDs: labelIDs, @@ -1030,10 +1040,17 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []in } return fmt.Errorf("newIssue: %v", err) } - - if err = sess.Commit(); err != nil { + if err := sess.Commit(); err != nil { return fmt.Errorf("Commit: %v", err) } + return nil +} + +// NewIssue creates new issue with labels for repository. +func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []int64, uuids []string) (err error) { + if err = NewSuppressedIssue(repo, issue, labelIDs, assigneeIDs, uuids); err != nil { + return err + } UpdateIssueIndexer(issue.ID) diff --git a/models/issue_comment.go b/models/issue_comment.go index a829c8066..502df7b93 100644 --- a/models/issue_comment.go +++ b/models/issue_comment.go @@ -83,8 +83,9 @@ const ( type Comment struct { ID int64 `xorm:"pk autoincr"` Type CommentType - PosterID int64 `xorm:"INDEX"` - Poster *User `xorm:"-"` + PosterID int64 `xorm:"INDEX"` + Poster *User `xorm:"-"` + GhostName string IssueID int64 `xorm:"INDEX"` Issue *Issue `xorm:"-"` LabelID int64 @@ -104,7 +105,7 @@ type Comment struct { Content string `xorm:"TEXT"` RenderedContent string `xorm:"-"` - CreatedUnix util.TimeStamp `xorm:"INDEX created"` + CreatedUnix util.TimeStamp `xorm:"INDEX"` UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` // Reference issue in commit message @@ -126,6 +127,13 @@ func (c *Comment) LoadIssue() (err error) { return } +// BeforeInsert is invoked before XORM inserts this +func (c *Comment) BeforeInsert() { + if c.CreatedUnix == util.TimeStamp(0) { + c.CreatedUnix = util.TimeStampNow() + } +} + // AfterLoad is invoked from XORM after setting the values of all fields of this object. func (c *Comment) AfterLoad(session *xorm.Session) { var err error @@ -139,6 +147,10 @@ func (c *Comment) AfterLoad(session *xorm.Session) { if IsErrUserNotExist(err) { c.PosterID = -1 c.Poster = NewGhostUser() + if len(c.GhostName) > 0 { + c.Poster.Name = c.GhostName + c.Poster.LowerName = strings.ToLower(c.GhostName) + } } else { log.Error(3, "getUserByID[%d]: %v", c.ID, err) } @@ -344,6 +356,10 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err Content: opts.Content, OldTitle: opts.OldTitle, NewTitle: opts.NewTitle, + CreatedUnix: opts.CreatedAt, + } + if opts.Doer.ID == -1 { + comment.GhostName = opts.Doer.Name } if _, err = e.Insert(comment); err != nil { return nil, err @@ -564,6 +580,7 @@ type CreateCommentOptions struct { LineNum int64 Content string Attachments []string // UUIDs of attachments + CreatedAt util.TimeStamp } // CreateComment creates comment of issue or commit. @@ -590,7 +607,7 @@ func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) { } // CreateIssueComment creates a plain issue comment. -func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string) (*Comment, error) { +func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string, createdAt util.TimeStamp) (*Comment, error) { comment, err := CreateComment(&CreateCommentOptions{ Type: CommentTypeComment, Doer: doer, @@ -598,6 +615,7 @@ func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content stri Issue: issue, Content: content, Attachments: attachments, + CreatedAt: createdAt, }) if err != nil { return nil, fmt.Errorf("CreateComment: %v", err) diff --git a/models/issue_list.go b/models/issue_list.go index 05130a6ee..1aa24de77 100644 --- a/models/issue_list.go +++ b/models/issue_list.go @@ -4,7 +4,10 @@ package models -import "fmt" +import ( + "fmt" + "strings" +) // IssueList defines a list of issues type IssueList []*Issue @@ -70,6 +73,11 @@ func (issues IssueList) loadPosters(e Engine) error { for _, issue := range issues { if issue.PosterID <= 0 { + if issue.GhostName != "" { + issue.Poster = NewGhostUser() + issue.Poster.Name = issue.GhostName + issue.Poster.LowerName = strings.ToLower(issue.GhostName) + } continue } var ok bool diff --git a/models/repo.go b/models/repo.go index c95c867f3..c7032721b 100644 --- a/models/repo.go +++ b/models/repo.go @@ -776,10 +776,21 @@ func (repo *Repository) getUsersWithAccessMode(e Engine, mode AccessMode) (_ []* } // NextIssueIndex returns the next issue index -// FIXME: should have a mutex to prevent producing same index for two issues that are created -// closely enough. -func (repo *Repository) NextIssueIndex() int64 { - return int64(repo.NumIssues+repo.NumPulls) + 1 +func (repo *Repository) NextIssueIndex() (latestIndex int64) { + var err error + if latestIndex, err = repo.latestIndex(x); err != nil { + log.Warn("latestIndex: %v", err) + return int64(repo.NumIssues+repo.NumPulls) + 1 + } + return latestIndex + 1 +} + +func (repo *Repository) latestIndex(e Engine) (int64, error) { + latestIdx := struct { + LatestIndex int64 + }{} + _, err := e.Table("issue").Select("MAX(`index`) as latest_index").Where("repo_id = ?", repo.ID).Get(&latestIdx) + return latestIdx.LatestIndex, err } var ( diff --git a/public/swagger.v1.json b/public/swagger.v1.json index 2c263ef1f..2b39d1c0e 100644 --- a/public/swagger.v1.json +++ b/public/swagger.v1.json @@ -1686,6 +1686,12 @@ "in": "path", "required": true }, + { + "type": "boolean", + "description": "suppresses notifications and webhooks if true. Requires repo admin permissions.", + "name": "suppress_notifications", + "in": "query" + }, { "name": "body", "in": "body", @@ -1697,6 +1703,9 @@ "responses": { "201": { "$ref": "#/responses/Issue" + }, + "422": { + "$ref": "#/responses/validationError" } } } @@ -1992,11 +2001,17 @@ "in": "path", "required": true }, + { + "type": "boolean", + "description": "suppresses notifications and webhooks if true. Requires repo admin permissions.", + "name": "suppress_notifications", + "in": "query" + }, { "name": "body", "in": "body", "schema": { - "$ref": "#/definitions/CreateIssueOption" + "$ref": "#/definitions/CreateIssueCommentOption" } } ], @@ -5567,6 +5582,17 @@ "body": { "type": "string", "x-go-name": "Body" + }, + "created_at": { + "description": "Created will be used as creation date. This is used for migration. Requires admin permissions.", + "type": "string", + "format": "date-time", + "x-go-name": "Created" + }, + "ghost_name": { + "description": "GhostName will be used if poster is not existing on Gitea. Requires admin permissions.", + "type": "string", + "x-go-name": "GhostName" } }, "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea" @@ -5598,11 +5624,28 @@ "type": "boolean", "x-go-name": "Closed" }, + "created_at": { + "description": "Created will be used as creation date. This is used for migration. Requires admin permissions.", + "type": "string", + "format": "date-time", + "x-go-name": "Created" + }, "due_date": { "type": "string", "format": "date-time", "x-go-name": "Deadline" }, + "ghost_name": { + "description": "GhostName is used if user is not existing on the gitea instance. Requires admin permissions.", + "type": "string", + "x-go-name": "GhostName" + }, + "index": { + "description": "Index is former index of the issue. If the index is already taken, an error will be returned. Requires admin permission. Use it only for migrations.", + "type": "integer", + "format": "int64", + "x-go-name": "Index" + }, "labels": { "description": "list of label ids", "type": "array", diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 211d8045a..dc908c9b3 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/modules/indexer" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" - api "code.gitea.io/sdk/gitea" ) @@ -156,6 +155,11 @@ func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) { // description: name of the repo // type: string // required: true + // - name: suppress_notifications + // in: query + // type: boolean + // description: suppresses notifications and webhooks if true. Requires repo admin permissions. + // required: false // - name: body // in: body // schema: @@ -163,6 +167,8 @@ func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) { // responses: // "201": // "$ref": "#/responses/Issue" + // "422": + // "$ref": "#/responses/validationError" var deadlineUnix util.TimeStamp if form.Deadline != nil { @@ -178,6 +184,33 @@ func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) { DeadlineUnix: deadlineUnix, } + if ctx.User.IsAdmin { + if form.Index != 0 { + if _, err := models.GetRawIssueByIndex(ctx.Repo.Repository.ID, form.Index); err == nil { + ctx.Error(422, "index is already in use", "index is already in use") + return + } else if err != nil && !models.IsErrIssueNotExist(err) { + ctx.Error(500, "GetRawIssueByIndex", err) + return + } + if form.Index <= 0 { + ctx.Error(422, "invalid index", "invalid index") + return + } + issue.Index = form.Index + } + issue.CreatedUnix = util.TimeStamp(form.Created.Unix()) + issue.GhostName = form.GhostName + if len(issue.GhostName) > 0 { + issue.PosterID = -1 + issue.Poster = &models.User{ + ID: -1, + Name: form.GhostName, + LowerName: strings.ToLower(form.GhostName), + } + } + } + // Get all assignee IDs assigneeIDs, err := models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees) if err != nil { @@ -188,21 +221,30 @@ func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) { } return } - - if err := models.NewIssue(ctx.Repo.Repository, issue, form.Labels, assigneeIDs, nil); err != nil { - if models.IsErrUserDoesNotHaveAccessToRepo(err) { - ctx.Error(400, "UserDoesNotHaveAccessToRepo", err) + if ctx.QueryBool("suppress_notifications") && ctx.User.IsAdminOfRepo(ctx.Repo.Repository) { + if err := models.NewSuppressedIssue(ctx.Repo.Repository, issue, form.Labels, assigneeIDs, nil); err != nil { + if models.IsErrUserDoesNotHaveAccessToRepo(err) { + ctx.Error(400, "UserDoesNotHaveAccessToRepo", err) + return + } + ctx.Error(500, "NewIssue", err) return } - ctx.Error(500, "NewIssue", err) - return - } - - if form.Closed { - if err := issue.ChangeStatus(ctx.User, ctx.Repo.Repository, true); err != nil { - ctx.Error(500, "ChangeStatus", err) + } else { + if err := models.NewIssue(ctx.Repo.Repository, issue, form.Labels, assigneeIDs, nil); err != nil { + if models.IsErrUserDoesNotHaveAccessToRepo(err) { + ctx.Error(400, "UserDoesNotHaveAccessToRepo", err) + return + } + ctx.Error(500, "NewIssue", err) return } + if form.Closed { + if err := issue.ChangeStatus(ctx.User, ctx.Repo.Repository, true); err != nil { + ctx.Error(500, "ChangeStatus", err) + return + } + } } // Refetch from database to assign some automatic values diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index 2865ea916..385a3039f 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -5,10 +5,12 @@ package repo import ( + "strings" "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/util" api "code.gitea.io/sdk/gitea" ) @@ -144,10 +146,15 @@ func CreateIssueComment(ctx *context.APIContext, form api.CreateIssueCommentOpti // description: index of the issue // type: integer // required: true + // - name: suppress_notifications + // in: query + // type: boolean + // description: suppresses notifications and webhooks if true. Requires repo admin permissions. + // required: false // - name: body // in: body // schema: - // "$ref": "#/definitions/CreateIssueOption" + // "$ref": "#/definitions/CreateIssueCommentOption" // responses: // "201": // "$ref": "#/responses/Comment" @@ -156,11 +163,37 @@ func CreateIssueComment(ctx *context.APIContext, form api.CreateIssueCommentOpti ctx.Error(500, "GetIssueByIndex", err) return } - - comment, err := models.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Body, nil) - if err != nil { - ctx.Error(500, "CreateIssueComment", err) - return + var comment *models.Comment + doer := ctx.User + createdUnix := util.TimeStamp(0) + if ctx.User.IsAdmin && len(form.GhostName) > 0 { + doer = &models.User{ + ID: -1, + Name: form.GhostName, + LowerName: strings.ToLower(form.GhostName), + } + if !form.Created.IsZero() { + createdUnix = util.TimeStamp(form.Created.Unix()) + } + } + if ctx.QueryBool("suppress_notifications") && ctx.User.IsAdminOfRepo(ctx.Repo.Repository) { + comment, err = models.CreateComment(&models.CreateCommentOptions{ + Type: models.CommentTypeComment, + Doer: doer, + Repo: ctx.Repo.Repository, + Issue: issue, + Content: form.Body, + CreatedAt: createdUnix, + }) + if err != nil { + ctx.Error(500, "CreateComment", err) + } + } else { + comment, err = models.CreateIssueComment(doer, ctx.Repo.Repository, issue, form.Body, nil, createdUnix) + if err != nil { + ctx.Error(500, "CreateIssueComment", err) + return + } } ctx.JSON(201, comment.APIFormat()) diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 18ab1691c..e5dc4fb03 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -1059,7 +1059,7 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) { return } - comment, err := models.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Content, attachments) + comment, err := models.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Content, attachments, util.TimeStamp(0)) if err != nil { ctx.ServerError("CreateIssueComment", err) return diff --git a/vendor/code.gitea.io/sdk/gitea/issue.go b/vendor/code.gitea.io/sdk/gitea/issue.go index fee7cd6f9..2bf3ffed9 100644 --- a/vendor/code.gitea.io/sdk/gitea/issue.go +++ b/vendor/code.gitea.io/sdk/gitea/issue.go @@ -103,6 +103,14 @@ type CreateIssueOption struct { // list of label ids Labels []int64 `json:"labels"` Closed bool `json:"closed"` + // GhostName is used if user is not existing on the gitea instance. Requires admin permissions. + GhostName string `json:"ghost_name" binding:"AlphaDashDot;MaxSize(35)"` + // Index is former index of the issue. If the index is already taken, an error will be returned. Requires admin permission. Use it only for migrations. + Index int64 `json:"index"` + // Created will be used as creation date. This is used for migration. Requires admin permissions. + // swagger:strfmt date-time + Created time.Time `json:"created_at"` + SuppressNotifications bool `json:"-"` } // CreateIssue create a new issue for a given repository @@ -112,7 +120,7 @@ func (c *Client) CreateIssue(owner, repo string, opt CreateIssueOption) (*Issue, return nil, err } issue := new(Issue) - return issue, c.getParsedResponse("POST", fmt.Sprintf("/repos/%s/%s/issues", owner, repo), + return issue, c.getParsedResponse("POST", fmt.Sprintf("/repos/%s/%s/issues?surpress_notifications=%t", owner, repo, opt.SuppressNotifications), jsonHeader, bytes.NewReader(body), issue) } diff --git a/vendor/code.gitea.io/sdk/gitea/issue_comment.go b/vendor/code.gitea.io/sdk/gitea/issue_comment.go index 2c8127c60..db8850b4a 100644 --- a/vendor/code.gitea.io/sdk/gitea/issue_comment.go +++ b/vendor/code.gitea.io/sdk/gitea/issue_comment.go @@ -41,6 +41,11 @@ func (c *Client) ListRepoIssueComments(owner, repo string) ([]*Comment, error) { type CreateIssueCommentOption struct { // required:true Body string `json:"body" binding:"Required"` + // GhostName will be used if poster is not existing on Gitea. Requires admin permissions. + GhostName string `json:"ghost_name" binding:"AlphaDashDot;MaxSize(35)"` + // Created will be used as creation date. This is used for migration. Requires admin permissions. + // swagger:strfmt date-time + Created time.Time `json:"created_at"` } // CreateIssueComment create comment on an issue. @@ -53,6 +58,16 @@ func (c *Client) CreateIssueComment(owner, repo string, index int64, opt CreateI return comment, c.getParsedResponse("POST", fmt.Sprintf("/repos/%s/%s/issues/%d/comments", owner, repo, index), jsonHeader, bytes.NewReader(body), comment) } +// CreateSuppressedIssueComment create comment on an issue without sending notifications if suppressed is true. Requires admin permissions for the repo. +func (c *Client) CreateSuppressedIssueComment(owner, repo string, index int64, suppressed bool, opt CreateIssueCommentOption) (*Comment, error) { + body, err := json.Marshal(&opt) + if err != nil { + return nil, err + } + comment := new(Comment) + return comment, c.getParsedResponse("POST", fmt.Sprintf("/repos/%s/%s/issues/%d/comments?surpress_notifications=%t", owner, repo, index, suppressed), jsonHeader, bytes.NewReader(body), comment) +} + // EditIssueCommentOption options for editing a comment type EditIssueCommentOption struct { // required: true