From 9bd9ad420582a7a34d18011847bb789f64271b1f Mon Sep 17 00:00:00 2001 From: Unknwon Date: Sun, 13 Mar 2016 23:20:22 -0400 Subject: [PATCH] #1692 add CRUD issue APIs - Fix go-gogs-client#10 - Related to #809 --- README.md | 2 +- cmd/web.go | 2 +- glide.lock | 4 +- gogs.go | 2 +- models/issue.go | 86 ++++++++++++---- modules/context/api.go | 26 +++++ routers/api/v1/admin/orgs.go | 2 +- routers/api/v1/admin/users.go | 4 +- routers/api/v1/api.go | 24 +++-- routers/api/v1/convert/convert.go | 113 +++++++++++++++----- routers/api/v1/org/org.go | 6 +- routers/api/v1/repo/branch.go | 4 +- routers/api/v1/repo/hooks.go | 7 +- routers/api/v1/repo/issue.go | 165 ++++++++++++++++++++++++++++++ routers/api/v1/repo/keys.go | 6 +- routers/api/v1/repo/repo.go | 12 +-- routers/api/v1/user/email.go | 4 +- routers/api/v1/user/followers.go | 2 +- routers/api/v1/user/keys.go | 6 +- routers/repo/issue.go | 51 +++------ routers/repo/pull.go | 11 +- routers/user/home.go | 5 - templates/.VERSION | 2 +- 23 files changed, 406 insertions(+), 140 deletions(-) create mode 100644 routers/api/v1/repo/issue.go diff --git a/README.md b/README.md index ff571bcf7..95302cea5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Gogs - Go Git Service [![Build Status](https://travis-ci.org/gogits/gogs.svg?bra ![](https://github.com/gogits/gogs/blob/master/public/img/gogs-large-resize.png?raw=true) -##### Current version: 0.9.8 +##### Current version: 0.9.9 | Web | UI | Preview | |:-------------:|:-------:|:-------:| diff --git a/cmd/web.go b/cmd/web.go index c57c223f1..b1d8b0fc8 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -88,7 +88,7 @@ func checkVersion() { {"gopkg.in/ini.v1", ini.Version, "1.8.4"}, {"gopkg.in/macaron.v1", macaron.Version, "1.1.2"}, {"github.com/gogits/git-module", git.Version, "0.2.9"}, - {"github.com/gogits/go-gogs-client", gogs.Version, "0.7.3"}, + {"github.com/gogits/go-gogs-client", gogs.Version, "0.7.4"}, } for _, c := range checkers { if !version.Compare(c.Version(), c.Expected, ">=") { diff --git a/glide.lock b/glide.lock index 7b0b9310e..d065409ca 100644 --- a/glide.lock +++ b/glide.lock @@ -35,7 +35,7 @@ imports: - name: github.com/go-xorm/core version: 502158401cde814951eae62f064d9e5ff39e13ce - name: github.com/go-xorm/xorm - version: bb8c5c04d7d65453c3f0176a307276857dccc3b0 + version: 99686972253975273816d1ebb4cdb0a53bf39100 - name: github.com/gogits/chardet version: 2404f777256163ea3eadb273dada5dcb037993c0 - name: github.com/gogits/cron @@ -43,7 +43,7 @@ imports: - name: github.com/gogits/git-module version: 76e8cce6c7ef3ba1cf75752261c721ebf14cd129 - name: github.com/gogits/go-gogs-client - version: d584b1e0fb4d8ad0e8cf2fae2368838f2526b408 + version: 788ec59749df076b98e208909b44fdef02779deb - name: github.com/issue9/identicon version: f8c0d2ce04db79c663b1da33d3a9f62be753ee88 - name: github.com/klauspost/compress diff --git a/gogs.go b/gogs.go index 062fa29bc..27d821f72 100644 --- a/gogs.go +++ b/gogs.go @@ -17,7 +17,7 @@ import ( "github.com/gogits/gogs/modules/setting" ) -const APP_VER = "0.9.8.0312" +const APP_VER = "0.9.9.0313" func init() { runtime.GOMAXPROCS(runtime.NumCPU()) diff --git a/models/issue.go b/models/issue.go index 067f0a0ba..edc46689d 100644 --- a/models/issue.go +++ b/models/issue.go @@ -88,6 +88,23 @@ func (i *Issue) AfterSet(colName string, _ xorm.Cell) { log.Error(3, "GetCommentsByIssueID[%d]: %v", i.ID, err) } + i.Labels, err = GetLabelsByIssueID(i.ID) + if err != nil { + log.Error(3, "GetLabelsByIssueID[%d]: %v", i.ID, err) + } + + case "poster_id": + i.Poster, err = GetUserByID(i.PosterID) + if err != nil { + if IsErrUserNotExist(err) { + i.PosterID = -1 + i.Poster = NewFakeUser() + } else { + log.Error(3, "GetUserByID[%d]: %v", i.ID, err) + } + return + } + case "milestone_id": if i.MilestoneID == 0 { return @@ -97,6 +114,7 @@ func (i *Issue) AfterSet(colName string, _ xorm.Cell) { if err != nil { log.Error(3, "GetMilestoneById[%d]: %v", i.ID, err) } + case "assignee_id": if i.AssigneeID == 0 { return @@ -106,6 +124,7 @@ func (i *Issue) AfterSet(colName string, _ xorm.Cell) { if err != nil { log.Error(3, "GetUserByID[%d]: %v", i.ID, err) } + case "deadline_unix": i.Deadline = time.Unix(i.DeadlineUnix, 0).Local() case "created_unix": @@ -120,21 +139,19 @@ func (i *Issue) HashTag() string { return "issue-" + com.ToStr(i.ID) } +// State returns string representation of issue status. +func (i *Issue) State() string { + if i.IsClosed { + return "closed" + } + return "open" +} + // IsPoster returns true if given user by ID is the poster. func (i *Issue) IsPoster(uid int64) bool { return i.PosterID == uid } -func (i *Issue) GetPoster() (err error) { - i.Poster, err = GetUserByID(i.PosterID) - if IsErrUserNotExist(err) { - i.PosterID = -1 - i.Poster = NewFakeUser() - return nil - } - return err -} - func (i *Issue) hasLabel(e Engine, labelID int64) bool { return hasIssueLabel(e, i.ID, labelID) } @@ -175,11 +192,6 @@ func (i *Issue) getLabels(e Engine) (err error) { return nil } -// GetLabels retrieves all labels of issue and assign to corresponding field. -func (i *Issue) GetLabels() error { - return i.getLabels(x) -} - func (i *Issue) removeLabel(e *xorm.Session, label *Label) error { return deleteIssueLabel(e, i, label) } @@ -303,6 +315,19 @@ func (i *Issue) GetPullRequest() (err error) { // It's caller's responsibility to create action. func newIssue(e *xorm.Session, repo *Repository, issue *Issue, labelIDs []int64, uuids []string, isPull bool) (err error) { + issue.Name = strings.TrimSpace(issue.Name) + issue.Index = repo.NextIssueIndex() + + if issue.AssigneeID > 0 { + // Silently drop invalid assignee + valid, err := hasAccess(e, &User{Id: issue.AssigneeID}, repo, ACCESS_MODE_WRITE) + if err != nil { + return fmt.Errorf("hasAccess: %v", err) + } else if !valid { + issue.AssigneeID = 0 + } + } + if _, err = e.Insert(issue); err != nil { return err } @@ -325,6 +350,10 @@ func newIssue(e *xorm.Session, repo *Repository, issue *Issue, labelIDs []int64, } for _, label := range labels { + if label.RepoID != repo.ID { + continue + } + if err = issue.addLabel(e, label); err != nil { return fmt.Errorf("addLabel: %v", err) } @@ -377,6 +406,10 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) return fmt.Errorf("newIssue: %v", err) } + if err = sess.Commit(); err != nil { + return fmt.Errorf("Commit: %v", err) + } + // Notify watchers. act := &Action{ ActUserID: issue.Poster.Id, @@ -389,11 +422,11 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) RepoName: repo.Name, IsPrivate: repo.IsPrivate, } - if err = notifyWatchers(sess, act); err != nil { - return err + if err = NotifyWatchers(act); err != nil { + log.Error(4, "notifyWatchers: %v", err) } - return sess.Commit() + return nil } // GetIssueByRef returns an Issue specified by a GFM reference. @@ -467,6 +500,10 @@ type IssuesOptions struct { // Issues returns a list of issues by given conditions. func Issues(opts *IssuesOptions) ([]*Issue, error) { + if opts.Page <= 0 { + opts.Page = 1 + } + sess := x.Limit(setting.IssuePagingNum, (opts.Page-1)*setting.IssuePagingNum) if opts.RepoID > 0 { @@ -997,6 +1034,9 @@ func (m *Milestone) BeforeUpdate() { func (m *Milestone) AfterSet(colName string, _ xorm.Cell) { switch colName { + case "num_closed_issues": + m.NumOpenIssues = m.NumIssues - m.NumClosedIssues + case "deadline_unix": m.Deadline = time.Unix(m.DeadlineUnix, 0).Local() if m.Deadline.Year() == 9999 { @@ -1007,14 +1047,18 @@ func (m *Milestone) AfterSet(colName string, _ xorm.Cell) { if time.Now().Local().After(m.Deadline) { m.IsOverDue = true } + case "closed_date_unix": m.ClosedDate = time.Unix(m.ClosedDateUnix, 0).Local() } } -// CalOpenIssues calculates the open issues of milestone. -func (m *Milestone) CalOpenIssues() { - m.NumOpenIssues = m.NumIssues - m.NumClosedIssues +// State returns string representation of milestone status. +func (m *Milestone) State() string { + if m.IsClosed { + return "closed" + } + return "open" } // NewMilestone creates new milestone of repository. diff --git a/modules/context/api.go b/modules/context/api.go index 44232d71a..8d036be9c 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -5,10 +5,15 @@ package context import ( + "fmt" + "strings" + + "github.com/Unknwon/paginater" "gopkg.in/macaron.v1" "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/setting" ) type APIContext struct { @@ -35,6 +40,27 @@ func (ctx *APIContext) Error(status int, title string, obj interface{}) { }) } +func (ctx *APIContext) SetLinkHeader(total, pageSize int) { + page := paginater.New(total, pageSize, ctx.QueryInt("page"), 0) + links := make([]string, 0, 4) + if page.HasNext() { + links = append(links, fmt.Sprintf("<%s%s?page=%d>; rel=\"next\"", setting.AppUrl, ctx.Req.URL.Path[1:], page.Next())) + } + if !page.IsLast() { + links = append(links, fmt.Sprintf("<%s%s?page=%d>; rel=\"last\"", setting.AppUrl, ctx.Req.URL.Path[1:], page.TotalPages())) + } + if !page.IsFirst() { + links = append(links, fmt.Sprintf("<%s%s?page=1>; rel=\"first\"", setting.AppUrl, ctx.Req.URL.Path[1:])) + } + if page.HasPrevious() { + links = append(links, fmt.Sprintf("<%s%s?page=%d>; rel=\"prev\"", setting.AppUrl, ctx.Req.URL.Path[1:], page.Previous())) + } + + if len(links) > 0 { + ctx.Header().Set("Link", strings.Join(links, ",")) + } +} + func APIContexter() macaron.Handler { return func(c *Context) { ctx := &APIContext{ diff --git a/routers/api/v1/admin/orgs.go b/routers/api/v1/admin/orgs.go index 1e7ad1565..fcce3b931 100644 --- a/routers/api/v1/admin/orgs.go +++ b/routers/api/v1/admin/orgs.go @@ -40,5 +40,5 @@ func CreateOrg(ctx *context.APIContext, form api.CreateOrgOption) { return } - ctx.JSON(201, convert.ToApiOrganization(org)) + ctx.JSON(201, convert.ToOrganization(org)) } diff --git a/routers/api/v1/admin/users.go b/routers/api/v1/admin/users.go index 50409cff9..ce13804ff 100644 --- a/routers/api/v1/admin/users.go +++ b/routers/api/v1/admin/users.go @@ -69,7 +69,7 @@ func CreateUser(ctx *context.APIContext, form api.CreateUserOption) { mailer.SendRegisterNotifyMail(ctx.Context.Context, u) } - ctx.JSON(201, convert.ToApiUser(u)) + ctx.JSON(201, convert.ToUser(u)) } // https://github.com/gogits/go-gogs-client/wiki/Administration-Users#edit-an-existing-user @@ -118,7 +118,7 @@ func EditUser(ctx *context.APIContext, form api.EditUserOption) { } log.Trace("Account profile updated by admin (%s): %s", ctx.User.Name, u.Name) - ctx.JSON(200, convert.ToApiUser(u)) + ctx.JSON(200, convert.ToUser(u)) } // https://github.com/gogits/go-gogs-client/wiki/Administration-Users#delete-a-user diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index a75910b8a..59625efdf 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -62,16 +62,18 @@ func RepoAssignment() macaron.Handler { return } - mode, err := models.AccessLevel(ctx.User, repo) - if err != nil { - ctx.Error(500, "AccessLevel", err) - return + if ctx.IsSigned && ctx.User.IsAdmin { + ctx.Repo.AccessMode = models.ACCESS_MODE_OWNER + } else { + mode, err := models.AccessLevel(ctx.User, repo) + if err != nil { + ctx.Error(500, "AccessLevel", err) + return + } + ctx.Repo.AccessMode = mode } - ctx.Repo.AccessMode = mode - - // Check access. - if ctx.Repo.AccessMode == models.ACCESS_MODE_NONE { + if !ctx.Repo.HasAccess() { ctx.Status(404) return } @@ -193,9 +195,9 @@ func RegisterRoutes(m *macaron.Macaron) { m.Combo("/:id").Get(repo.GetDeployKey). Delete(repo.DeleteDeploykey) }) - m.Group("/issue", func() { - m.Combo("").Get().Post() - m.Combo("/:index").Get().Patch() + m.Group("/issues", func() { + m.Combo("").Get(repo.ListIssues).Post(bind(api.CreateIssueOption{}), repo.CreateIssue) + m.Combo("/:index").Get(repo.GetIssue).Patch(bind(api.EditIssueOption{}), repo.EditIssue) }) }, RepoAssignment()) }, ReqToken()) diff --git a/routers/api/v1/convert/convert.go b/routers/api/v1/convert/convert.go index 57b3d44e2..7e3e380bd 100644 --- a/routers/api/v1/convert/convert.go +++ b/routers/api/v1/convert/convert.go @@ -9,15 +9,19 @@ import ( "github.com/Unknwon/com" - api "github.com/gogits/go-gogs-client" "github.com/gogits/git-module" + api "github.com/gogits/go-gogs-client" "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/setting" ) -// ToApiUser converts user to its API format. -func ToApiUser(u *models.User) *api.User { +func ToUser(u *models.User) *api.User { + if u == nil { + return nil + } + return &api.User{ ID: u.Id, UserName: u.Name, @@ -27,7 +31,7 @@ func ToApiUser(u *models.User) *api.User { } } -func ToApiEmail(email *models.EmailAddress) *api.Email { +func ToEmail(email *models.EmailAddress) *api.Email { return &api.Email{ Email: email.Email, Verified: email.IsActivated, @@ -35,12 +39,11 @@ func ToApiEmail(email *models.EmailAddress) *api.Email { } } -// ToApiRepository converts repository to API format. -func ToApiRepository(owner *models.User, repo *models.Repository, permission api.Permission) *api.Repository { +func ToRepository(owner *models.User, repo *models.Repository, permission api.Permission) *api.Repository { cl := repo.CloneLink() return &api.Repository{ - Id: repo.ID, - Owner: *ToApiUser(owner), + ID: repo.ID, + Owner: ToUser(owner), FullName: owner.Name + "/" + repo.Name, Private: repo.IsPrivate, Fork: repo.IsFork, @@ -51,30 +54,27 @@ func ToApiRepository(owner *models.User, repo *models.Repository, permission api } } -// ToApiBranch converts user to its API format. -func ToApiBranch(b *models.Branch,c *git.Commit) *api.Branch { +func ToBranch(b *models.Branch, c *git.Commit) *api.Branch { return &api.Branch{ - Name: b.Name, - Commit: ToApiCommit(c), - } + Name: b.Name, + Commit: ToCommit(c), + } } -// ToApiCommit converts user to its API format. -func ToApiCommit(c *git.Commit) *api.PayloadCommit { +func ToCommit(c *git.Commit) *api.PayloadCommit { return &api.PayloadCommit{ - ID: c.ID.String(), + ID: c.ID.String(), Message: c.Message(), - URL: "Not implemented", + URL: "Not implemented", Author: &api.PayloadAuthor{ - Name: c.Committer.Name, + Name: c.Committer.Name, Email: c.Committer.Email, /* UserName: c.Committer.UserName, */ }, } } -// ToApiPublicKey converts public key to its API format. -func ToApiPublicKey(apiLink string, key *models.PublicKey) *api.PublicKey { +func ToPublicKey(apiLink string, key *models.PublicKey) *api.PublicKey { return &api.PublicKey{ ID: key.ID, Key: key.Content, @@ -84,8 +84,7 @@ func ToApiPublicKey(apiLink string, key *models.PublicKey) *api.PublicKey { } } -// ToApiHook converts webhook to its API format. -func ToApiHook(repoLink string, w *models.Webhook) *api.Hook { +func ToHook(repoLink string, w *models.Webhook) *api.Hook { config := map[string]string{ "url": w.URL, "content_type": w.ContentType.Name(), @@ -110,8 +109,7 @@ func ToApiHook(repoLink string, w *models.Webhook) *api.Hook { } } -// ToApiDeployKey converts deploy key to its API format. -func ToApiDeployKey(apiLink string, key *models.DeployKey) *api.DeployKey { +func ToDeployKey(apiLink string, key *models.DeployKey) *api.DeployKey { return &api.DeployKey{ ID: key.ID, Key: key.Content, @@ -122,7 +120,72 @@ func ToApiDeployKey(apiLink string, key *models.DeployKey) *api.DeployKey { } } -func ToApiOrganization(org *models.User) *api.Organization { +func ToLabel(label *models.Label) *api.Label { + return &api.Label{ + Name: label.Name, + Color: label.Color, + } +} + +func ToMilestone(milestone *models.Milestone) *api.Milestone { + if milestone == nil { + return nil + } + + apiMilestone := &api.Milestone{ + ID: milestone.ID, + State: milestone.State(), + Title: milestone.Name, + Description: milestone.Content, + OpenIssues: milestone.NumOpenIssues, + ClosedIssues: milestone.NumClosedIssues, + } + if milestone.IsClosed { + apiMilestone.Closed = &milestone.ClosedDate + } + if milestone.Deadline.Year() < 9999 { + apiMilestone.Deadline = &milestone.Deadline + } + return apiMilestone +} + +func ToIssue(issue *models.Issue) *api.Issue { + apiLabels := make([]*api.Label, len(issue.Labels)) + for i := range issue.Labels { + apiLabels[i] = ToLabel(issue.Labels[i]) + } + + apiIssue := &api.Issue{ + ID: issue.ID, + Index: issue.Index, + State: issue.State(), + Title: issue.Name, + Body: issue.Content, + User: ToUser(issue.Poster), + Labels: apiLabels, + Assignee: ToUser(issue.Assignee), + Milestone: ToMilestone(issue.Milestone), + Comments: issue.NumComments, + Created: issue.Created, + Updated: issue.Updated, + } + if issue.IsPull { + if err := issue.GetPullRequest(); err != nil { + log.Error(4, "GetPullRequest", err) + } else { + apiIssue.PullRequest = &api.PullRequestMeta{ + HasMerged: issue.PullRequest.HasMerged, + } + if issue.PullRequest.HasMerged { + apiIssue.PullRequest.Merged = &issue.PullRequest.Merged + } + } + } + + return apiIssue +} + +func ToOrganization(org *models.User) *api.Organization { return &api.Organization{ ID: org.Id, AvatarUrl: org.AvatarLink(), diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index 4fdc5e96a..b4e52d486 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -21,7 +21,7 @@ func listUserOrgs(ctx *context.APIContext, u *models.User, all bool) { apiOrgs := make([]*api.Organization, len(u.Orgs)) for i := range u.Orgs { - apiOrgs[i] = convert.ToApiOrganization(u.Orgs[i]) + apiOrgs[i] = convert.ToOrganization(u.Orgs[i]) } ctx.JSON(200, &apiOrgs) } @@ -46,7 +46,7 @@ func Get(ctx *context.APIContext) { if ctx.Written() { return } - ctx.JSON(200, convert.ToApiOrganization(org)) + ctx.JSON(200, convert.ToOrganization(org)) } // https://github.com/gogits/go-gogs-client/wiki/Organizations#edit-an-organization @@ -70,5 +70,5 @@ func Edit(ctx *context.APIContext, form api.EditOrgOption) { return } - ctx.JSON(200, convert.ToApiOrganization(org)) + ctx.JSON(200, convert.ToOrganization(org)) } diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index eedef8015..a141921ea 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -25,7 +25,7 @@ func GetBranch(ctx *context.APIContext) { return } - ctx.JSON(200, convert.ToApiBranch(branch, c)) + ctx.JSON(200, convert.ToBranch(branch, c)) } // https://github.com/gogits/go-gogs-client/wiki/Repositories#list-branches @@ -43,7 +43,7 @@ func ListBranches(ctx *context.APIContext) { ctx.Error(500, "GetCommit", err) return } - apiBranches[i] = convert.ToApiBranch(branches[i], c) + apiBranches[i] = convert.ToBranch(branches[i], c) } ctx.JSON(200, &apiBranches) diff --git a/routers/api/v1/repo/hooks.go b/routers/api/v1/repo/hooks.go index 840f258cf..0cbe6762a 100644 --- a/routers/api/v1/repo/hooks.go +++ b/routers/api/v1/repo/hooks.go @@ -26,9 +26,8 @@ func ListHooks(ctx *context.APIContext) { apiHooks := make([]*api.Hook, len(hooks)) for i := range hooks { - apiHooks[i] = convert.ToApiHook(ctx.Repo.RepoLink, hooks[i]) + apiHooks[i] = convert.ToHook(ctx.Repo.RepoLink, hooks[i]) } - ctx.JSON(200, &apiHooks) } @@ -94,7 +93,7 @@ func CreateHook(ctx *context.APIContext, form api.CreateHookOption) { return } - ctx.JSON(201, convert.ToApiHook(ctx.Repo.RepoLink, w)) + ctx.JSON(201, convert.ToHook(ctx.Repo.RepoLink, w)) } // https://github.com/gogits/go-gogs-client/wiki/Repositories#edit-a-hook @@ -161,5 +160,5 @@ func EditHook(ctx *context.APIContext, form api.EditHookOption) { return } - ctx.JSON(200, convert.ToApiHook(ctx.Repo.RepoLink, w)) + ctx.JSON(200, convert.ToHook(ctx.Repo.RepoLink, w)) } diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go new file mode 100644 index 000000000..bdef1297d --- /dev/null +++ b/routers/api/v1/repo/issue.go @@ -0,0 +1,165 @@ +// Copyright 2016 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package repo + +import ( + "fmt" + "strings" + + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/context" + "github.com/gogits/gogs/modules/setting" + "github.com/gogits/gogs/routers/api/v1/convert" + "github.com/gogits/gogs/routers/repo" +) + +func ListIssues(ctx *context.APIContext) { + issues, err := models.Issues(&models.IssuesOptions{ + RepoID: ctx.Repo.Repository.ID, + Page: ctx.QueryInt("page"), + }) + if err != nil { + ctx.Error(500, "Issues", err) + return + } + + apiIssues := make([]*api.Issue, len(issues)) + for i := range issues { + apiIssues[i] = convert.ToIssue(issues[i]) + } + + ctx.SetLinkHeader(ctx.Repo.Repository.NumIssues, setting.IssuePagingNum) + ctx.JSON(200, &apiIssues) +} + +func GetIssue(ctx *context.APIContext) { + issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + if err != nil { + if models.IsErrIssueNotExist(err) { + ctx.Status(404) + } else { + ctx.Error(500, "GetIssueByIndex", err) + } + return + } + + ctx.JSON(200, convert.ToIssue(issue)) +} + +func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) { + issue := &models.Issue{ + RepoID: ctx.Repo.Repository.ID, + Name: form.Title, + PosterID: ctx.User.Id, + Poster: ctx.User, + Content: form.Body, + } + + if ctx.Repo.IsWriter() { + if len(form.Assignee) > 0 { + assignee, err := models.GetUserByName(form.Assignee) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.Error(422, "", fmt.Sprintf("Assignee does not exist: [name: %s]", form.Assignee)) + } else { + ctx.Error(500, "GetUserByName", err) + } + return + } + issue.AssigneeID = assignee.Id + } + issue.MilestoneID = form.Milestone + } else { + form.Labels = nil + } + + if err := models.NewIssue(ctx.Repo.Repository, issue, form.Labels, nil); err != nil { + ctx.Error(500, "NewIssue", err) + return + } else if err := repo.MailWatchersAndMentions(ctx.Context, issue); err != nil { + ctx.Error(500, "MailWatchersAndMentions", err) + return + } + + // Refetch from database to assign some automatic values + var err error + issue, err = models.GetIssueByID(issue.ID) + if err != nil { + ctx.Error(500, "GetIssueByID", err) + return + } + ctx.JSON(201, convert.ToIssue(issue)) +} + +func EditIssue(ctx *context.APIContext, form api.EditIssueOption) { + issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + if err != nil { + if models.IsErrIssueNotExist(err) { + ctx.Status(404) + } else { + ctx.Error(500, "GetIssueByIndex", err) + } + return + } + + if !issue.IsPoster(ctx.User.Id) && !ctx.Repo.IsWriter() { + ctx.Status(403) + return + } + + if len(form.Title) > 0 { + issue.Name = form.Title + } + if form.Body != nil { + issue.Content = *form.Body + } + + if ctx.Repo.IsWriter() && form.Assignee != nil && + (issue.Assignee == nil || issue.Assignee.LowerName != strings.ToLower(*form.Assignee)) { + if len(*form.Assignee) == 0 { + issue.AssigneeID = 0 + } else { + assignee, err := models.GetUserByName(*form.Assignee) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.Error(422, "", fmt.Sprintf("Assignee does not exist: [name: %s]", *form.Assignee)) + } else { + ctx.Error(500, "GetUserByName", err) + } + return + } + issue.AssigneeID = assignee.Id + } + + if err = models.UpdateIssueUserByAssignee(issue); err != nil { + ctx.Error(500, "UpdateIssueUserByAssignee", err) + return + } + } + if ctx.Repo.IsWriter() && form.Milestone != nil && + issue.MilestoneID != *form.Milestone { + oldMid := issue.MilestoneID + issue.MilestoneID = *form.Milestone + if err = models.ChangeMilestoneAssign(oldMid, issue); err != nil { + ctx.Error(500, "ChangeMilestoneAssign", err) + return + } + } + + if err = models.UpdateIssue(issue); err != nil { + ctx.Error(500, "UpdateIssue", err) + return + } + + // Refetch from database to assign some automatic values + issue, err = models.GetIssueByID(issue.ID) + if err != nil { + ctx.Error(500, "GetIssueByID", err) + return + } + ctx.JSON(201, convert.ToIssue(issue)) +} diff --git a/routers/api/v1/repo/keys.go b/routers/api/v1/repo/keys.go index d8ae137c7..563dac261 100644 --- a/routers/api/v1/repo/keys.go +++ b/routers/api/v1/repo/keys.go @@ -34,7 +34,7 @@ func ListDeployKeys(ctx *context.APIContext) { ctx.Error(500, "GetContent", err) return } - apiKeys[i] = convert.ToApiDeployKey(apiLink, keys[i]) + apiKeys[i] = convert.ToDeployKey(apiLink, keys[i]) } ctx.JSON(200, &apiKeys) @@ -58,7 +58,7 @@ func GetDeployKey(ctx *context.APIContext) { } apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name) - ctx.JSON(200, convert.ToApiDeployKey(apiLink, key)) + ctx.JSON(200, convert.ToDeployKey(apiLink, key)) } func HandleCheckKeyStringError(ctx *context.APIContext, err error) { @@ -96,7 +96,7 @@ func CreateDeployKey(ctx *context.APIContext, form api.CreateKeyOption) { key.Content = content apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name) - ctx.JSON(201, convert.ToApiDeployKey(apiLink, key)) + ctx.JSON(201, convert.ToDeployKey(apiLink, key)) } // https://github.com/gogits/go-gogs-client/wiki/Repositories-Deploy-Keys#remove-a-deploy-key diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 8c79c32e2..90a323c25 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -69,7 +69,7 @@ func Search(ctx *context.APIContext) { return } results[i] = &api.Repository{ - Id: repos[i].ID, + ID: repos[i].ID, FullName: path.Join(repos[i].Owner.Name, repos[i].Name), } } @@ -97,12 +97,12 @@ func ListMyRepos(ctx *context.APIContext) { repos := make([]*api.Repository, numOwnRepos+len(accessibleRepos)) for i := range ownRepos { - repos[i] = convert.ToApiRepository(ctx.User, ownRepos[i], api.Permission{true, true, true}) + repos[i] = convert.ToRepository(ctx.User, ownRepos[i], api.Permission{true, true, true}) } i := numOwnRepos for repo, access := range accessibleRepos { - repos[i] = convert.ToApiRepository(repo.Owner, repo, api.Permission{ + repos[i] = convert.ToRepository(repo.Owner, repo, api.Permission{ Admin: access >= models.ACCESS_MODE_ADMIN, Push: access >= models.ACCESS_MODE_WRITE, Pull: true, @@ -139,7 +139,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *models.User, opt api.CreateR return } - ctx.JSON(201, convert.ToApiRepository(owner, repo, api.Permission{true, true, true})) + ctx.JSON(201, convert.ToRepository(owner, repo, api.Permission{true, true, true})) } // https://github.com/gogits/go-gogs-client/wiki/Repositories#create @@ -239,7 +239,7 @@ func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) { } log.Trace("Repository migrated: %s/%s", ctxUser.Name, form.RepoName) - ctx.JSON(201, convert.ToApiRepository(ctxUser, repo, api.Permission{true, true, true})) + ctx.JSON(201, convert.ToRepository(ctxUser, repo, api.Permission{true, true, true})) } func parseOwnerAndRepo(ctx *context.APIContext) (*models.User, *models.Repository) { @@ -273,7 +273,7 @@ func Get(ctx *context.APIContext) { return } - ctx.JSON(200, convert.ToApiRepository(owner, repo, api.Permission{true, true, true})) + ctx.JSON(200, convert.ToRepository(owner, repo, api.Permission{true, true, true})) } // https://github.com/gogits/go-gogs-client/wiki/Repositories#delete diff --git a/routers/api/v1/user/email.go b/routers/api/v1/user/email.go index fe11954ba..6a3d525cc 100644 --- a/routers/api/v1/user/email.go +++ b/routers/api/v1/user/email.go @@ -22,7 +22,7 @@ func ListEmails(ctx *context.APIContext) { } apiEmails := make([]*api.Email, len(emails)) for i := range emails { - apiEmails[i] = convert.ToApiEmail(emails[i]) + apiEmails[i] = convert.ToEmail(emails[i]) } ctx.JSON(200, &apiEmails) } @@ -54,7 +54,7 @@ func AddEmail(ctx *context.APIContext, form api.CreateEmailOption) { apiEmails := make([]*api.Email, len(emails)) for i := range emails { - apiEmails[i] = convert.ToApiEmail(emails[i]) + apiEmails[i] = convert.ToEmail(emails[i]) } ctx.JSON(201, &apiEmails) } diff --git a/routers/api/v1/user/followers.go b/routers/api/v1/user/followers.go index 7e00cb3f1..00d1952d5 100644 --- a/routers/api/v1/user/followers.go +++ b/routers/api/v1/user/followers.go @@ -15,7 +15,7 @@ import ( func responseApiUsers(ctx *context.APIContext, users []*models.User) { apiUsers := make([]*api.User, len(users)) for i := range users { - apiUsers[i] = convert.ToApiUser(users[i]) + apiUsers[i] = convert.ToUser(users[i]) } ctx.JSON(200, &apiUsers) } diff --git a/routers/api/v1/user/keys.go b/routers/api/v1/user/keys.go index 3e1ed666e..7337112ec 100644 --- a/routers/api/v1/user/keys.go +++ b/routers/api/v1/user/keys.go @@ -46,7 +46,7 @@ func listPublicKeys(ctx *context.APIContext, uid int64) { apiLink := composePublicKeysAPILink() apiKeys := make([]*api.PublicKey, len(keys)) for i := range keys { - apiKeys[i] = convert.ToApiPublicKey(apiLink, keys[i]) + apiKeys[i] = convert.ToPublicKey(apiLink, keys[i]) } ctx.JSON(200, &apiKeys) @@ -79,7 +79,7 @@ func GetPublicKey(ctx *context.APIContext) { } apiLink := composePublicKeysAPILink() - ctx.JSON(200, convert.ToApiPublicKey(apiLink, key)) + ctx.JSON(200, convert.ToPublicKey(apiLink, key)) } // CreateUserPublicKey creates new public key to given user by ID. @@ -96,7 +96,7 @@ func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid return } apiLink := composePublicKeysAPILink() - ctx.JSON(201, convert.ToApiPublicKey(apiLink, key)) + ctx.JSON(201, convert.ToPublicKey(apiLink, key)) } // https://github.com/gogits/go-gogs-client/wiki/Users-Public-Keys#create-a-public-key diff --git a/routers/repo/issue.go b/routers/repo/issue.go index d66123f91..7c6db13ad 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -195,16 +195,6 @@ func Issues(ctx *context.Context) { // Get posters. for i := range issues { - if err = issues[i].GetPoster(); err != nil { - ctx.Handle(500, "GetPoster", fmt.Errorf("[#%d]%v", issues[i].ID, err)) - return - } - - if err = issues[i].GetLabels(); err != nil { - ctx.Handle(500, "GetLabels", fmt.Errorf("[#%d]%v", issues[i].ID, err)) - return - } - if !ctx.IsSigned { issues[i].IsRead = true continue @@ -405,7 +395,7 @@ func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm) ([]int64 return labelIDs, milestoneID, assigneeID } -func notifyWatchersAndMentions(ctx *context.Context, issue *models.Issue) { +func MailWatchersAndMentions(ctx *context.Context, issue *models.Issue) error { // Update mentions mentions := markdown.MentionPattern.FindAllString(issue.Content, -1) if len(mentions) > 0 { @@ -414,8 +404,7 @@ func notifyWatchersAndMentions(ctx *context.Context, issue *models.Issue) { } if err := models.UpdateMentions(mentions, issue.ID); err != nil { - ctx.Handle(500, "UpdateMentions", err) - return + return fmt.Errorf("UpdateMentions: %v", err) } } @@ -425,8 +414,7 @@ func notifyWatchersAndMentions(ctx *context.Context, issue *models.Issue) { if setting.Service.EnableNotifyMail { tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, repo, issue) if err != nil { - ctx.Handle(500, "SendIssueNotifyMail", err) - return + return fmt.Errorf("SendIssueNotifyMail: %v", err) } tos = append(tos, ctx.User.LowerName) @@ -440,10 +428,11 @@ func notifyWatchersAndMentions(ctx *context.Context, issue *models.Issue) { } if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner, repo, issue, models.GetUserEmailsByNames(newTos)); err != nil { - ctx.Handle(500, "SendIssueMentionMail", err) - return + return fmt.Errorf("SendIssueMentionMail: %v", err) } } + + return nil } func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) { @@ -471,9 +460,8 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) { } issue := &models.Issue{ - RepoID: ctx.Repo.Repository.ID, - Index: repo.NextIssueIndex(), - Name: strings.TrimSpace(form.Title), + RepoID: repo.ID, + Name: form.Title, PosterID: ctx.User.Id, Poster: ctx.User, MilestoneID: milestoneID, @@ -483,10 +471,8 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) { if err := models.NewIssue(repo, issue, labelIDs, attachments); err != nil { ctx.Handle(500, "NewIssue", err) return - } - - notifyWatchersAndMentions(ctx, issue) - if ctx.Written() { + } else if err := MailWatchersAndMentions(ctx, issue); err != nil { + ctx.Handle(500, "MailWatchersAndMentions", err) return } @@ -586,10 +572,6 @@ func ViewIssue(ctx *context.Context) { ctx.Data["PageIsIssueList"] = true } - if err = issue.GetPoster(); err != nil { - ctx.Handle(500, "GetPoster", err) - return - } issue.RenderedContent = string(markdown.Render([]byte(issue.Content), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())) @@ -610,10 +592,6 @@ func ViewIssue(ctx *context.Context) { // Metas. // Check labels. - if err = issue.GetLabels(); err != nil { - ctx.Handle(500, "GetLabels", err) - return - } labelIDMark := make(map[int64]bool) for i := range issue.Labels { labelIDMark[issue.Labels[i].ID] = true @@ -698,7 +676,7 @@ func ViewIssue(ctx *context.Context) { ctx.Data["Participants"] = participants ctx.Data["NumParticipants"] = len(participants) ctx.Data["Issue"] = issue - ctx.Data["IsIssueOwner"] = ctx.Repo.IsWriter() || (ctx.IsSigned && (issue.IsPoster(ctx.User.Id) || ctx.User.IsAdmin)) + ctx.Data["IsIssueOwner"] = ctx.Repo.IsWriter() || (ctx.IsSigned && issue.IsPoster(ctx.User.Id)) ctx.Data["SignInLink"] = setting.AppSubUrl + "/user/login" ctx.Data["RequireHighlightJS"] = true @@ -725,7 +703,7 @@ func UpdateIssueTitle(ctx *context.Context) { return } - if !ctx.IsSigned || (ctx.User.Id != issue.PosterID && !ctx.Repo.IsWriter() && !ctx.User.IsAdmin) { + if !ctx.IsSigned || (!issue.IsPoster(ctx.User.Id) && !ctx.Repo.IsWriter()) { ctx.Error(403) return } @@ -752,7 +730,7 @@ func UpdateIssueContent(ctx *context.Context) { return } - if !ctx.IsSigned || (ctx.User.Id != issue.PosterID && !ctx.Repo.IsWriter() && !ctx.User.IsAdmin) { + if !ctx.IsSigned || (ctx.User.Id != issue.PosterID && !ctx.Repo.IsWriter()) { ctx.Error(403) return } @@ -955,7 +933,7 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) { return } - notifyWatchersAndMentions(ctx, &models.Issue{ + MailWatchersAndMentions(ctx, &models.Issue{ ID: issue.ID, Index: issue.Index, Name: issue.Name, @@ -1099,7 +1077,6 @@ func Milestones(ctx *context.Context) { } for _, m := range miles { m.RenderedContent = string(markdown.Render([]byte(m.Content), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())) - m.CalOpenIssues() } ctx.Data["Milestones"] = miles diff --git a/routers/repo/pull.go b/routers/repo/pull.go index c2147bbaf..8ad4f3f8e 100644 --- a/routers/repo/pull.go +++ b/routers/repo/pull.go @@ -156,10 +156,7 @@ func checkPullInfo(ctx *context.Context) *models.Issue { return nil } - if err = issue.GetPoster(); err != nil { - ctx.Handle(500, "GetPoster", err) - return nil - } else if err = issue.GetPullRequest(); err != nil { + if err = issue.GetPullRequest(); err != nil { ctx.Handle(500, "GetPullRequest", err) return nil } else if err = issue.GetHeadRepo(); err != nil { @@ -682,10 +679,8 @@ func CompareAndPullRequestPost(ctx *context.Context, form auth.CreateIssueForm) } else if err := pullRequest.PushToBaseRepo(); err != nil { ctx.Handle(500, "PushToBaseRepo", err) return - } - - notifyWatchersAndMentions(ctx, pullIssue) - if ctx.Written() { + } else if err := MailWatchersAndMentions(ctx, pullIssue); err != nil { + ctx.Handle(500, "MailWatchersAndMentions", err) return } diff --git a/routers/user/home.go b/routers/user/home.go index 2a7f219e4..0c48d6cc2 100644 --- a/routers/user/home.go +++ b/routers/user/home.go @@ -284,11 +284,6 @@ func Issues(ctx *context.Context) { ctx.Handle(500, "GetOwner", fmt.Errorf("[#%d]%v", issues[i].ID, err)) return } - - if err = issues[i].GetPoster(); err != nil { - ctx.Handle(500, "GetPoster", fmt.Errorf("[#%d]%v", issues[i].ID, err)) - return - } } ctx.Data["Issues"] = issues diff --git a/templates/.VERSION b/templates/.VERSION index bde4be41b..9989d2631 100644 --- a/templates/.VERSION +++ b/templates/.VERSION @@ -1 +1 @@ -0.9.8.0312 \ No newline at end of file +0.9.9.0313 \ No newline at end of file