diff options
Diffstat (limited to 'internal/route/api/v1')
28 files changed, 3136 insertions, 0 deletions
diff --git a/internal/route/api/v1/admin/org.go b/internal/route/api/v1/admin/org.go new file mode 100644 index 00000000..758f41e6 --- /dev/null +++ b/internal/route/api/v1/admin/org.go @@ -0,0 +1,17 @@ +// Copyright 2015 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 admin + +import ( + api "github.com/gogs/go-gogs-client" + org2 "gogs.io/gogs/internal/route/api/v1/org" + user2 "gogs.io/gogs/internal/route/api/v1/user" + + "gogs.io/gogs/internal/context" +) + +func CreateOrg(c *context.APIContext, form api.CreateOrgOption) { + org2.CreateOrgForUser(c, form, user2.GetUserByParams(c)) +} diff --git a/internal/route/api/v1/admin/org_repo.go b/internal/route/api/v1/admin/org_repo.go new file mode 100644 index 00000000..b17b1462 --- /dev/null +++ b/internal/route/api/v1/admin/org_repo.go @@ -0,0 +1,46 @@ +// 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 admin + +import ( + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/db/errors" +) + +func GetRepositoryByParams(c *context.APIContext) *db.Repository { + repo, err := db.GetRepositoryByName(c.Org.Team.OrgID, c.Params(":reponame")) + if err != nil { + c.NotFoundOrServerError("GetRepositoryByName", errors.IsRepoNotExist, err) + return nil + } + return repo +} + +func AddTeamRepository(c *context.APIContext) { + repo := GetRepositoryByParams(c) + if c.Written() { + return + } + if err := c.Org.Team.AddRepository(repo); err != nil { + c.ServerError("AddRepository", err) + return + } + + c.NoContent() +} + +func RemoveTeamRepository(c *context.APIContext) { + repo := GetRepositoryByParams(c) + if c.Written() { + return + } + if err := c.Org.Team.RemoveRepository(repo.ID); err != nil { + c.ServerError("RemoveRepository", err) + return + } + + c.NoContent() +} diff --git a/internal/route/api/v1/admin/org_team.go b/internal/route/api/v1/admin/org_team.go new file mode 100644 index 00000000..e81b6b1b --- /dev/null +++ b/internal/route/api/v1/admin/org_team.go @@ -0,0 +1,62 @@ +// 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 admin + +import ( + convert2 "gogs.io/gogs/internal/route/api/v1/convert" + user2 "gogs.io/gogs/internal/route/api/v1/user" + "net/http" + + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" +) + +func CreateTeam(c *context.APIContext, form api.CreateTeamOption) { + team := &db.Team{ + OrgID: c.Org.Organization.ID, + Name: form.Name, + Description: form.Description, + Authorize: db.ParseAccessMode(form.Permission), + } + if err := db.NewTeam(team); err != nil { + if db.IsErrTeamAlreadyExist(err) { + c.Error(http.StatusUnprocessableEntity, "", err) + } else { + c.ServerError("NewTeam", err) + } + return + } + + c.JSON(http.StatusCreated, convert2.ToTeam(team)) +} + +func AddTeamMember(c *context.APIContext) { + u := user2.GetUserByParams(c) + if c.Written() { + return + } + if err := c.Org.Team.AddMember(u.ID); err != nil { + c.ServerError("AddMember", err) + return + } + + c.NoContent() +} + +func RemoveTeamMember(c *context.APIContext) { + u := user2.GetUserByParams(c) + if c.Written() { + return + } + + if err := c.Org.Team.RemoveMember(u.ID); err != nil { + c.ServerError("RemoveMember", err) + return + } + + c.NoContent() +} diff --git a/internal/route/api/v1/admin/repo.go b/internal/route/api/v1/admin/repo.go new file mode 100644 index 00000000..68a26297 --- /dev/null +++ b/internal/route/api/v1/admin/repo.go @@ -0,0 +1,22 @@ +// Copyright 2015 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 admin + +import ( + api "github.com/gogs/go-gogs-client" + repo2 "gogs.io/gogs/internal/route/api/v1/repo" + user2 "gogs.io/gogs/internal/route/api/v1/user" + + "gogs.io/gogs/internal/context" +) + +func CreateRepo(c *context.APIContext, form api.CreateRepoOption) { + owner := user2.GetUserByParams(c) + if c.Written() { + return + } + + repo2.CreateUserRepo(c, owner, form) +} diff --git a/internal/route/api/v1/admin/user.go b/internal/route/api/v1/admin/user.go new file mode 100644 index 00000000..5e291df2 --- /dev/null +++ b/internal/route/api/v1/admin/user.go @@ -0,0 +1,159 @@ +// Copyright 2015 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 admin + +import ( + user2 "gogs.io/gogs/internal/route/api/v1/user" + "net/http" + + log "gopkg.in/clog.v1" + + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/db/errors" + "gogs.io/gogs/internal/mailer" + "gogs.io/gogs/internal/setting" +) + +func parseLoginSource(c *context.APIContext, u *db.User, sourceID int64, loginName string) { + if sourceID == 0 { + return + } + + source, err := db.GetLoginSourceByID(sourceID) + if err != nil { + if errors.IsLoginSourceNotExist(err) { + c.Error(http.StatusUnprocessableEntity, "", err) + } else { + c.ServerError("GetLoginSourceByID", err) + } + return + } + + u.LoginType = source.Type + u.LoginSource = source.ID + u.LoginName = loginName +} + +func CreateUser(c *context.APIContext, form api.CreateUserOption) { + u := &db.User{ + Name: form.Username, + FullName: form.FullName, + Email: form.Email, + Passwd: form.Password, + IsActive: true, + LoginType: db.LOGIN_PLAIN, + } + + parseLoginSource(c, u, form.SourceID, form.LoginName) + if c.Written() { + return + } + + if err := db.CreateUser(u); err != nil { + if db.IsErrUserAlreadyExist(err) || + db.IsErrEmailAlreadyUsed(err) || + db.IsErrNameReserved(err) || + db.IsErrNamePatternNotAllowed(err) { + c.Error(http.StatusUnprocessableEntity, "", err) + } else { + c.ServerError("CreateUser", err) + } + return + } + log.Trace("Account created by admin %q: %s", c.User.Name, u.Name) + + // Send email notification. + if form.SendNotify && setting.MailService != nil { + mailer.SendRegisterNotifyMail(c.Context.Context, db.NewMailerUser(u)) + } + + c.JSON(http.StatusCreated, u.APIFormat()) +} + +func EditUser(c *context.APIContext, form api.EditUserOption) { + u := user2.GetUserByParams(c) + if c.Written() { + return + } + + parseLoginSource(c, u, form.SourceID, form.LoginName) + if c.Written() { + return + } + + if len(form.Password) > 0 { + u.Passwd = form.Password + var err error + if u.Salt, err = db.GetUserSalt(); err != nil { + c.ServerError("GetUserSalt", err) + return + } + u.EncodePasswd() + } + + u.LoginName = form.LoginName + u.FullName = form.FullName + u.Email = form.Email + u.Website = form.Website + u.Location = form.Location + if form.Active != nil { + u.IsActive = *form.Active + } + if form.Admin != nil { + u.IsAdmin = *form.Admin + } + if form.AllowGitHook != nil { + u.AllowGitHook = *form.AllowGitHook + } + if form.AllowImportLocal != nil { + u.AllowImportLocal = *form.AllowImportLocal + } + if form.MaxRepoCreation != nil { + u.MaxRepoCreation = *form.MaxRepoCreation + } + + if err := db.UpdateUser(u); err != nil { + if db.IsErrEmailAlreadyUsed(err) { + c.Error(http.StatusUnprocessableEntity, "", err) + } else { + c.ServerError("UpdateUser", err) + } + return + } + log.Trace("Account profile updated by admin %q: %s", c.User.Name, u.Name) + + c.JSONSuccess(u.APIFormat()) +} + +func DeleteUser(c *context.APIContext) { + u := user2.GetUserByParams(c) + if c.Written() { + return + } + + if err := db.DeleteUser(u); err != nil { + if db.IsErrUserOwnRepos(err) || + db.IsErrUserHasOrgs(err) { + c.Error(http.StatusUnprocessableEntity, "", err) + } else { + c.ServerError("DeleteUser", err) + } + return + } + log.Trace("Account deleted by admin(%s): %s", c.User.Name, u.Name) + + c.NoContent() +} + +func CreatePublicKey(c *context.APIContext, form api.CreateKeyOption) { + u := user2.GetUserByParams(c) + if c.Written() { + return + } + user2.CreateUserPublicKey(c, form, u.ID) +} diff --git a/internal/route/api/v1/api.go b/internal/route/api/v1/api.go new file mode 100644 index 00000000..56e7bcd2 --- /dev/null +++ b/internal/route/api/v1/api.go @@ -0,0 +1,406 @@ +// Copyright 2015 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 v1 + +import ( + admin2 "gogs.io/gogs/internal/route/api/v1/admin" + misc2 "gogs.io/gogs/internal/route/api/v1/misc" + org2 "gogs.io/gogs/internal/route/api/v1/org" + repo2 "gogs.io/gogs/internal/route/api/v1/repo" + user2 "gogs.io/gogs/internal/route/api/v1/user" + "net/http" + "strings" + + "github.com/go-macaron/binding" + "gopkg.in/macaron.v1" + + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/db/errors" + "gogs.io/gogs/internal/form" +) + +// repoAssignment extracts information from URL parameters to retrieve the repository, +// and makes sure the context user has at least the read access to the repository. +func repoAssignment() macaron.Handler { + return func(c *context.APIContext) { + username := c.Params(":username") + reponame := c.Params(":reponame") + + var err error + var owner *db.User + + // Check if the context user is the repository owner. + if c.IsLogged && c.User.LowerName == strings.ToLower(username) { + owner = c.User + } else { + owner, err = db.GetUserByName(username) + if err != nil { + c.NotFoundOrServerError("GetUserByName", errors.IsUserNotExist, err) + return + } + } + c.Repo.Owner = owner + + r, err := db.GetRepositoryByName(owner.ID, reponame) + if err != nil { + c.NotFoundOrServerError("GetRepositoryByName", errors.IsRepoNotExist, err) + return + } else if err = r.GetOwner(); err != nil { + c.ServerError("GetOwner", err) + return + } + + if c.IsTokenAuth && c.User.IsAdmin { + c.Repo.AccessMode = db.ACCESS_MODE_OWNER + } else { + mode, err := db.UserAccessMode(c.UserID(), r) + if err != nil { + c.ServerError("UserAccessMode", err) + return + } + c.Repo.AccessMode = mode + } + + if !c.Repo.HasAccess() { + c.NotFound() + return + } + + c.Repo.Repository = r + } +} + +// orgAssignment extracts information from URL parameters to retrieve the organization or team. +func orgAssignment(args ...bool) macaron.Handler { + var ( + assignOrg bool + assignTeam bool + ) + if len(args) > 0 { + assignOrg = args[0] + } + if len(args) > 1 { + assignTeam = args[1] + } + return func(c *context.APIContext) { + c.Org = new(context.APIOrganization) + + var err error + if assignOrg { + c.Org.Organization, err = db.GetUserByName(c.Params(":orgname")) + if err != nil { + c.NotFoundOrServerError("GetUserByName", errors.IsUserNotExist, err) + return + } + } + + if assignTeam { + c.Org.Team, err = db.GetTeamByID(c.ParamsInt64(":teamid")) + if err != nil { + c.NotFoundOrServerError("GetTeamByID", errors.IsTeamNotExist, err) + return + } + } + } +} + +// reqToken makes sure the context user is authorized via access token. +func reqToken() macaron.Handler { + return func(c *context.Context) { + if !c.IsTokenAuth { + c.Error(http.StatusUnauthorized) + return + } + } +} + +// reqBasicAuth makes sure the context user is authorized via HTTP Basic Auth. +func reqBasicAuth() macaron.Handler { + return func(c *context.Context) { + if !c.IsBasicAuth { + c.Error(http.StatusUnauthorized) + return + } + } +} + +// reqAdmin makes sure the context user is a site admin. +func reqAdmin() macaron.Handler { + return func(c *context.Context) { + if !c.IsLogged || !c.User.IsAdmin { + c.Error(http.StatusForbidden) + return + } + } +} + +// reqRepoWriter makes sure the context user has at least write access to the repository. +func reqRepoWriter() macaron.Handler { + return func(c *context.Context) { + if !c.Repo.IsWriter() { + c.Error(http.StatusForbidden) + return + } + } +} + +// reqRepoWriter makes sure the context user has at least admin access to the repository. +func reqRepoAdmin() macaron.Handler { + return func(c *context.Context) { + if !c.Repo.IsAdmin() { + c.Error(http.StatusForbidden) + return + } + } +} + +func mustEnableIssues(c *context.APIContext) { + if !c.Repo.Repository.EnableIssues || c.Repo.Repository.EnableExternalTracker { + c.NotFound() + return + } +} + +// RegisterRoutes registers all route in API v1 to the web application. +// FIXME: custom form error response +func RegisterRoutes(m *macaron.Macaron) { + bind := binding.Bind + + m.Group("/v1", func() { + // Handle preflight OPTIONS request + m.Options("/*", func() {}) + + // Miscellaneous + m.Post("/markdown", bind(api.MarkdownOption{}), misc2.Markdown) + m.Post("/markdown/raw", misc2.MarkdownRaw) + + // Users + m.Group("/users", func() { + m.Get("/search", user2.Search) + + m.Group("/:username", func() { + m.Get("", user2.GetInfo) + + m.Group("/tokens", func() { + m.Combo(""). + Get(user2.ListAccessTokens). + Post(bind(api.CreateAccessTokenOption{}), user2.CreateAccessToken) + }, reqBasicAuth()) + }) + }) + + m.Group("/users", func() { + m.Group("/:username", func() { + m.Get("/keys", user2.ListPublicKeys) + + m.Get("/followers", user2.ListFollowers) + m.Group("/following", func() { + m.Get("", user2.ListFollowing) + m.Get("/:target", user2.CheckFollowing) + }) + }) + }, reqToken()) + + m.Group("/user", func() { + m.Get("", user2.GetAuthenticatedUser) + m.Combo("/emails"). + Get(user2.ListEmails). + Post(bind(api.CreateEmailOption{}), user2.AddEmail). + Delete(bind(api.CreateEmailOption{}), user2.DeleteEmail) + + m.Get("/followers", user2.ListMyFollowers) + m.Group("/following", func() { + m.Get("", user2.ListMyFollowing) + m.Combo("/:username"). + Get(user2.CheckMyFollowing). + Put(user2.Follow). + Delete(user2.Unfollow) + }) + + m.Group("/keys", func() { + m.Combo(""). + Get(user2.ListMyPublicKeys). + Post(bind(api.CreateKeyOption{}), user2.CreatePublicKey) + m.Combo("/:id"). + Get(user2.GetPublicKey). + Delete(user2.DeletePublicKey) + }) + + m.Get("/issues", repo2.ListUserIssues) + }, reqToken()) + + // Repositories + m.Get("/users/:username/repos", reqToken(), repo2.ListUserRepositories) + m.Get("/orgs/:org/repos", reqToken(), repo2.ListOrgRepositories) + m.Combo("/user/repos", reqToken()). + Get(repo2.ListMyRepos). + Post(bind(api.CreateRepoOption{}), repo2.Create) + m.Post("/org/:org/repos", reqToken(), bind(api.CreateRepoOption{}), repo2.CreateOrgRepo) + + m.Group("/repos", func() { + m.Get("/search", repo2.Search) + + m.Get("/:username/:reponame", repoAssignment(), repo2.Get) + }) + + m.Group("/repos", func() { + m.Post("/migrate", bind(form.MigrateRepo{}), repo2.Migrate) + m.Delete("/:username/:reponame", repoAssignment(), repo2.Delete) + + m.Group("/:username/:reponame", func() { + m.Group("/hooks", func() { + m.Combo(""). + Get(repo2.ListHooks). + Post(bind(api.CreateHookOption{}), repo2.CreateHook) + m.Combo("/:id"). + Patch(bind(api.EditHookOption{}), repo2.EditHook). + Delete(repo2.DeleteHook) + }, reqRepoAdmin()) + + m.Group("/collaborators", func() { + m.Get("", repo2.ListCollaborators) + m.Combo("/:collaborator"). + Get(repo2.IsCollaborator). + Put(bind(api.AddCollaboratorOption{}), repo2.AddCollaborator). + Delete(repo2.DeleteCollaborator) + }, reqRepoAdmin()) + + m.Get("/raw/*", context.RepoRef(), repo2.GetRawFile) + m.Get("/archive/*", repo2.GetArchive) + m.Get("/forks", repo2.ListForks) + m.Group("/branches", func() { + m.Get("", repo2.ListBranches) + m.Get("/*", repo2.GetBranch) + }) + m.Group("/commits", func() { + m.Get("/:sha", repo2.GetSingleCommit) + m.Get("/*", repo2.GetReferenceSHA) + }) + + m.Group("/keys", func() { + m.Combo(""). + Get(repo2.ListDeployKeys). + Post(bind(api.CreateKeyOption{}), repo2.CreateDeployKey) + m.Combo("/:id"). + Get(repo2.GetDeployKey). + Delete(repo2.DeleteDeploykey) + }, reqRepoAdmin()) + + m.Group("/issues", func() { + m.Combo(""). + Get(repo2.ListIssues). + Post(bind(api.CreateIssueOption{}), repo2.CreateIssue) + m.Group("/comments", func() { + m.Get("", repo2.ListRepoIssueComments) + m.Patch("/:id", bind(api.EditIssueCommentOption{}), repo2.EditIssueComment) + }) + m.Group("/:index", func() { + m.Combo(""). + Get(repo2.GetIssue). + Patch(bind(api.EditIssueOption{}), repo2.EditIssue) + + m.Group("/comments", func() { + m.Combo(""). + Get(repo2.ListIssueComments). + Post(bind(api.CreateIssueCommentOption{}), repo2.CreateIssueComment) + m.Combo("/:id"). + Patch(bind(api.EditIssueCommentOption{}), repo2.EditIssueComment). + Delete(repo2.DeleteIssueComment) + }) + + m.Get("/labels", repo2.ListIssueLabels) + m.Group("/labels", func() { + m.Combo(""). + Post(bind(api.IssueLabelsOption{}), repo2.AddIssueLabels). + Put(bind(api.IssueLabelsOption{}), repo2.ReplaceIssueLabels). + Delete(repo2.ClearIssueLabels) + m.Delete("/:id", repo2.DeleteIssueLabel) + }, reqRepoWriter()) + }) + }, mustEnableIssues) + + m.Group("/labels", func() { + m.Get("", repo2.ListLabels) + m.Get("/:id", repo2.GetLabel) + }) + m.Group("/labels", func() { + m.Post("", bind(api.CreateLabelOption{}), repo2.CreateLabel) + m.Combo("/:id"). + Patch(bind(api.EditLabelOption{}), repo2.EditLabel). + Delete(repo2.DeleteLabel) + }, reqRepoWriter()) + + m.Group("/milestones", func() { + m.Get("", repo2.ListMilestones) + m.Get("/:id", repo2.GetMilestone) + }) + m.Group("/milestones", func() { + m.Post("", bind(api.CreateMilestoneOption{}), repo2.CreateMilestone) + m.Combo("/:id"). + Patch(bind(api.EditMilestoneOption{}), repo2.EditMilestone). + Delete(repo2.DeleteMilestone) + }, reqRepoWriter()) + + m.Patch("/issue-tracker", reqRepoWriter(), bind(api.EditIssueTrackerOption{}), repo2.IssueTracker) + m.Post("/mirror-sync", reqRepoWriter(), repo2.MirrorSync) + m.Get("/editorconfig/:filename", context.RepoRef(), repo2.GetEditorconfig) + }, repoAssignment()) + }, reqToken()) + + m.Get("/issues", reqToken(), repo2.ListUserIssues) + + // Organizations + m.Combo("/user/orgs", reqToken()). + Get(org2.ListMyOrgs). + Post(bind(api.CreateOrgOption{}), org2.CreateMyOrg) + + m.Get("/users/:username/orgs", org2.ListUserOrgs) + m.Group("/orgs/:orgname", func() { + m.Combo(""). + Get(org2.Get). + Patch(bind(api.EditOrgOption{}), org2.Edit) + m.Get("/teams", org2.ListTeams) + }, orgAssignment(true)) + + m.Group("/admin", func() { + m.Group("/users", func() { + m.Post("", bind(api.CreateUserOption{}), admin2.CreateUser) + + m.Group("/:username", func() { + m.Combo(""). + Patch(bind(api.EditUserOption{}), admin2.EditUser). + Delete(admin2.DeleteUser) + m.Post("/keys", bind(api.CreateKeyOption{}), admin2.CreatePublicKey) + m.Post("/orgs", bind(api.CreateOrgOption{}), admin2.CreateOrg) + m.Post("/repos", bind(api.CreateRepoOption{}), admin2.CreateRepo) + }) + }) + + m.Group("/orgs/:orgname", func() { + m.Group("/teams", func() { + m.Post("", orgAssignment(true), bind(api.CreateTeamOption{}), admin2.CreateTeam) + }) + }) + + m.Group("/teams", func() { + m.Group("/:teamid", func() { + m.Combo("/members/:username"). + Put(admin2.AddTeamMember). + Delete(admin2.RemoveTeamMember) + m.Combo("/repos/:reponame"). + Put(admin2.AddTeamRepository). + Delete(admin2.RemoveTeamRepository) + }, orgAssignment(false, true)) + }) + }, reqAdmin()) + + m.Any("/*", func(c *context.Context) { + c.NotFound() + }) + }, context.APIContexter()) +} diff --git a/internal/route/api/v1/convert/convert.go b/internal/route/api/v1/convert/convert.go new file mode 100644 index 00000000..0bc2a5ca --- /dev/null +++ b/internal/route/api/v1/convert/convert.go @@ -0,0 +1,127 @@ +// Copyright 2015 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 convert + +import ( + "fmt" + + "github.com/unknwon/com" + + "github.com/gogs/git-module" + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/db" +) + +func ToEmail(email *db.EmailAddress) *api.Email { + return &api.Email{ + Email: email.Email, + Verified: email.IsActivated, + Primary: email.IsPrimary, + } +} + +func ToBranch(b *db.Branch, c *git.Commit) *api.Branch { + return &api.Branch{ + Name: b.Name, + Commit: ToCommit(c), + } +} + +func ToCommit(c *git.Commit) *api.PayloadCommit { + authorUsername := "" + author, err := db.GetUserByEmail(c.Author.Email) + if err == nil { + authorUsername = author.Name + } + committerUsername := "" + committer, err := db.GetUserByEmail(c.Committer.Email) + if err == nil { + committerUsername = committer.Name + } + return &api.PayloadCommit{ + ID: c.ID.String(), + Message: c.Message(), + URL: "Not implemented", + Author: &api.PayloadUser{ + Name: c.Author.Name, + Email: c.Author.Email, + UserName: authorUsername, + }, + Committer: &api.PayloadUser{ + Name: c.Committer.Name, + Email: c.Committer.Email, + UserName: committerUsername, + }, + Timestamp: c.Author.When, + } +} + +func ToPublicKey(apiLink string, key *db.PublicKey) *api.PublicKey { + return &api.PublicKey{ + ID: key.ID, + Key: key.Content, + URL: apiLink + com.ToStr(key.ID), + Title: key.Name, + Created: key.Created, + } +} + +func ToHook(repoLink string, w *db.Webhook) *api.Hook { + config := map[string]string{ + "url": w.URL, + "content_type": w.ContentType.Name(), + } + if w.HookTaskType == db.SLACK { + s := w.GetSlackHook() + config["channel"] = s.Channel + config["username"] = s.Username + config["icon_url"] = s.IconURL + config["color"] = s.Color + } + + return &api.Hook{ + ID: w.ID, + Type: w.HookTaskType.Name(), + URL: fmt.Sprintf("%s/settings/hooks/%d", repoLink, w.ID), + Active: w.IsActive, + Config: config, + Events: w.EventsArray(), + Updated: w.Updated, + Created: w.Created, + } +} + +func ToDeployKey(apiLink string, key *db.DeployKey) *api.DeployKey { + return &api.DeployKey{ + ID: key.ID, + Key: key.Content, + URL: apiLink + com.ToStr(key.ID), + Title: key.Name, + Created: key.Created, + ReadOnly: true, // All deploy keys are read-only. + } +} + +func ToOrganization(org *db.User) *api.Organization { + return &api.Organization{ + ID: org.ID, + AvatarUrl: org.AvatarLink(), + UserName: org.Name, + FullName: org.FullName, + Description: org.Description, + Website: org.Website, + Location: org.Location, + } +} + +func ToTeam(team *db.Team) *api.Team { + return &api.Team{ + ID: team.ID, + Name: team.Name, + Description: team.Description, + Permission: team.Authorize.String(), + } +} diff --git a/internal/route/api/v1/convert/utils.go b/internal/route/api/v1/convert/utils.go new file mode 100644 index 00000000..01b3f246 --- /dev/null +++ b/internal/route/api/v1/convert/utils.go @@ -0,0 +1,19 @@ +// 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 convert + +import ( + "gogs.io/gogs/internal/setting" +) + +// ToCorrectPageSize makes sure page size is in allowed range. +func ToCorrectPageSize(size int) int { + if size <= 0 { + size = 10 + } else if size > setting.API.MaxResponseItems { + size = setting.API.MaxResponseItems + } + return size +} diff --git a/internal/route/api/v1/misc/markdown.go b/internal/route/api/v1/misc/markdown.go new file mode 100644 index 00000000..8731e32b --- /dev/null +++ b/internal/route/api/v1/misc/markdown.go @@ -0,0 +1,42 @@ +// Copyright 2014 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 misc + +import ( + "net/http" + + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/markup" +) + +func Markdown(c *context.APIContext, form api.MarkdownOption) { + if c.HasApiError() { + c.Error(http.StatusUnprocessableEntity, "", c.GetErrMsg()) + return + } + + if len(form.Text) == 0 { + c.Write([]byte("")) + return + } + + switch form.Mode { + case "gfm": + c.Write(markup.Markdown([]byte(form.Text), form.Context, nil)) + default: + c.Write(markup.RawMarkdown([]byte(form.Text), "")) + } +} + +func MarkdownRaw(c *context.APIContext) { + body, err := c.Req.Body().Bytes() + if err != nil { + c.Error(http.StatusUnprocessableEntity, "", err) + return + } + c.Write(markup.RawMarkdown(body, "")) +} diff --git a/internal/route/api/v1/org/org.go b/internal/route/api/v1/org/org.go new file mode 100644 index 00000000..dbb3e4dd --- /dev/null +++ b/internal/route/api/v1/org/org.go @@ -0,0 +1,96 @@ +// Copyright 2015 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 org + +import ( + convert2 "gogs.io/gogs/internal/route/api/v1/convert" + user2 "gogs.io/gogs/internal/route/api/v1/user" + "net/http" + + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" +) + +func CreateOrgForUser(c *context.APIContext, apiForm api.CreateOrgOption, user *db.User) { + if c.Written() { + return + } + + org := &db.User{ + Name: apiForm.UserName, + FullName: apiForm.FullName, + Description: apiForm.Description, + Website: apiForm.Website, + Location: apiForm.Location, + IsActive: true, + Type: db.USER_TYPE_ORGANIZATION, + } + if err := db.CreateOrganization(org, user); err != nil { + if db.IsErrUserAlreadyExist(err) || + db.IsErrNameReserved(err) || + db.IsErrNamePatternNotAllowed(err) { + c.Error(http.StatusUnprocessableEntity, "", err) + } else { + c.ServerError("CreateOrganization", err) + } + return + } + + c.JSON(201, convert2.ToOrganization(org)) +} + +func listUserOrgs(c *context.APIContext, u *db.User, all bool) { + if err := u.GetOrganizations(all); err != nil { + c.ServerError("GetOrganizations", err) + return + } + + apiOrgs := make([]*api.Organization, len(u.Orgs)) + for i := range u.Orgs { + apiOrgs[i] = convert2.ToOrganization(u.Orgs[i]) + } + c.JSONSuccess(&apiOrgs) +} + +func ListMyOrgs(c *context.APIContext) { + listUserOrgs(c, c.User, true) +} + +func CreateMyOrg(c *context.APIContext, apiForm api.CreateOrgOption) { + CreateOrgForUser(c, apiForm, c.User) +} + +func ListUserOrgs(c *context.APIContext) { + u := user2.GetUserByParams(c) + if c.Written() { + return + } + listUserOrgs(c, u, false) +} + +func Get(c *context.APIContext) { + c.JSONSuccess(convert2.ToOrganization(c.Org.Organization)) +} + +func Edit(c *context.APIContext, form api.EditOrgOption) { + org := c.Org.Organization + if !org.IsOwnedBy(c.User.ID) { + c.Status(http.StatusForbidden) + return + } + + org.FullName = form.FullName + org.Description = form.Description + org.Website = form.Website + org.Location = form.Location + if err := db.UpdateUser(org); err != nil { + c.ServerError("UpdateUser", err) + return + } + + c.JSONSuccess(convert2.ToOrganization(org)) +} diff --git a/internal/route/api/v1/org/team.go b/internal/route/api/v1/org/team.go new file mode 100644 index 00000000..528e6183 --- /dev/null +++ b/internal/route/api/v1/org/team.go @@ -0,0 +1,26 @@ +// 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 org + +import ( + api "github.com/gogs/go-gogs-client" + convert2 "gogs.io/gogs/internal/route/api/v1/convert" + + "gogs.io/gogs/internal/context" +) + +func ListTeams(c *context.APIContext) { + org := c.Org.Organization + if err := org.GetTeams(); err != nil { + c.Error(500, "GetTeams", err) + return + } + + apiTeams := make([]*api.Team, len(org.Teams)) + for i := range org.Teams { + apiTeams[i] = convert2.ToTeam(org.Teams[i]) + } + c.JSON(200, apiTeams) +} diff --git a/internal/route/api/v1/repo/branch.go b/internal/route/api/v1/repo/branch.go new file mode 100644 index 00000000..b90d1e24 --- /dev/null +++ b/internal/route/api/v1/repo/branch.go @@ -0,0 +1,55 @@ +// 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 ( + api "github.com/gogs/go-gogs-client" + convert2 "gogs.io/gogs/internal/route/api/v1/convert" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db/errors" +) + +// https://github.com/gogs/go-gogs-client/wiki/Repositories#get-branch +func GetBranch(c *context.APIContext) { + branch, err := c.Repo.Repository.GetBranch(c.Params("*")) + if err != nil { + if errors.IsErrBranchNotExist(err) { + c.Error(404, "GetBranch", err) + } else { + c.Error(500, "GetBranch", err) + } + return + } + + commit, err := branch.GetCommit() + if err != nil { + c.Error(500, "GetCommit", err) + return + } + + c.JSON(200, convert2.ToBranch(branch, commit)) +} + +// https://github.com/gogs/go-gogs-client/wiki/Repositories#list-branches +func ListBranches(c *context.APIContext) { + branches, err := c.Repo.Repository.GetBranches() + if err != nil { + c.Error(500, "GetBranches", err) + return + } + + apiBranches := make([]*api.Branch, len(branches)) + for i := range branches { + commit, err := branches[i].GetCommit() + if err != nil { + c.Error(500, "GetCommit", err) + return + } + apiBranches[i] = convert2.ToBranch(branches[i], commit) + } + + c.JSON(200, &apiBranches) +} diff --git a/internal/route/api/v1/repo/collaborators.go b/internal/route/api/v1/repo/collaborators.go new file mode 100644 index 00000000..e8f74848 --- /dev/null +++ b/internal/route/api/v1/repo/collaborators.go @@ -0,0 +1,90 @@ +// 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 ( + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/db/errors" +) + +func ListCollaborators(c *context.APIContext) { + collaborators, err := c.Repo.Repository.GetCollaborators() + if err != nil { + c.ServerError("GetCollaborators", err) + return + } + + apiCollaborators := make([]*api.Collaborator, len(collaborators)) + for i := range collaborators { + apiCollaborators[i] = collaborators[i].APIFormat() + } + c.JSONSuccess(&apiCollaborators) +} + +func AddCollaborator(c *context.APIContext, form api.AddCollaboratorOption) { + collaborator, err := db.GetUserByName(c.Params(":collaborator")) + if err != nil { + if errors.IsUserNotExist(err) { + c.Error(422, "", err) + } else { + c.Error(500, "GetUserByName", err) + } + return + } + + if err := c.Repo.Repository.AddCollaborator(collaborator); err != nil { + c.Error(500, "AddCollaborator", err) + return + } + + if form.Permission != nil { + if err := c.Repo.Repository.ChangeCollaborationAccessMode(collaborator.ID, db.ParseAccessMode(*form.Permission)); err != nil { + c.Error(500, "ChangeCollaborationAccessMode", err) + return + } + } + + c.Status(204) +} + +func IsCollaborator(c *context.APIContext) { + collaborator, err := db.GetUserByName(c.Params(":collaborator")) + if err != nil { + if errors.IsUserNotExist(err) { + c.Error(422, "", err) + } else { + c.Error(500, "GetUserByName", err) + } + return + } + + if !c.Repo.Repository.IsCollaborator(collaborator.ID) { + c.Status(404) + } else { + c.Status(204) + } +} + +func DeleteCollaborator(c *context.APIContext) { + collaborator, err := db.GetUserByName(c.Params(":collaborator")) + if err != nil { + if errors.IsUserNotExist(err) { + c.Error(422, "", err) + } else { + c.Error(500, "GetUserByName", err) + } + return + } + + if err := c.Repo.Repository.DeleteCollaboration(collaborator.ID); err != nil { + c.Error(500, "DeleteCollaboration", err) + return + } + + c.Status(204) +} diff --git a/internal/route/api/v1/repo/commits.go b/internal/route/api/v1/repo/commits.go new file mode 100644 index 00000000..55bfc045 --- /dev/null +++ b/internal/route/api/v1/repo/commits.go @@ -0,0 +1,138 @@ +// Copyright 2018 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 ( + "net/http" + "strings" + "time" + + "github.com/gogs/git-module" + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/db/errors" + "gogs.io/gogs/internal/setting" +) + +func GetSingleCommit(c *context.APIContext) { + if strings.Contains(c.Req.Header.Get("Accept"), api.MediaApplicationSHA) { + c.SetParams("*", c.Params(":sha")) + GetReferenceSHA(c) + return + } + + gitRepo, err := git.OpenRepository(c.Repo.Repository.RepoPath()) + if err != nil { + c.ServerError("OpenRepository", err) + return + } + commit, err := gitRepo.GetCommit(c.Params(":sha")) + if err != nil { + c.NotFoundOrServerError("GetCommit", git.IsErrNotExist, err) + return + } + + // Retrieve author and committer information + var apiAuthor, apiCommitter *api.User + author, err := db.GetUserByEmail(commit.Author.Email) + if err != nil && !errors.IsUserNotExist(err) { + c.ServerError("Get user by author email", err) + return + } else if err == nil { + apiAuthor = author.APIFormat() + } + // Save one query if the author is also the committer + if commit.Committer.Email == commit.Author.Email { + apiCommitter = apiAuthor + } else { + committer, err := db.GetUserByEmail(commit.Committer.Email) + if err != nil && !errors.IsUserNotExist(err) { + c.ServerError("Get user by committer email", err) + return + } else if err == nil { + apiCommitter = committer.APIFormat() + } + } + + // Retrieve parent(s) of the commit + apiParents := make([]*api.CommitMeta, commit.ParentCount()) + for i := 0; i < commit.ParentCount(); i++ { + sha, _ := commit.ParentID(i) + apiParents[i] = &api.CommitMeta{ + URL: c.BaseURL + "/repos/" + c.Repo.Repository.FullName() + "/commits/" + sha.String(), + SHA: sha.String(), + } + } + + c.JSONSuccess(&api.Commit{ + CommitMeta: &api.CommitMeta{ + URL: setting.AppURL + c.Link[1:], + SHA: commit.ID.String(), + }, + HTMLURL: c.Repo.Repository.HTMLURL() + "/commits/" + commit.ID.String(), + RepoCommit: &api.RepoCommit{ + URL: setting.AppURL + c.Link[1:], + Author: &api.CommitUser{ + Name: commit.Author.Name, + Email: commit.Author.Email, + Date: commit.Author.When.Format(time.RFC3339), + }, + Committer: &api.CommitUser{ + Name: commit.Committer.Name, + Email: commit.Committer.Email, + Date: commit.Committer.When.Format(time.RFC3339), + }, + Message: commit.Summary(), + Tree: &api.CommitMeta{ + URL: c.BaseURL + "/repos/" + c.Repo.Repository.FullName() + "/tree/" + commit.ID.String(), + SHA: commit.ID.String(), + }, + }, + Author: apiAuthor, + Committer: apiCommitter, + Parents: apiParents, + }) +} + +func GetReferenceSHA(c *context.APIContext) { + gitRepo, err := git.OpenRepository(c.Repo.Repository.RepoPath()) + if err != nil { + c.ServerError("OpenRepository", err) + return + } + + ref := c.Params("*") + refType := 0 // 0-undetermined, 1-branch, 2-tag + if strings.HasPrefix(ref, git.BRANCH_PREFIX) { + ref = strings.TrimPrefix(ref, git.BRANCH_PREFIX) + refType = 1 + } else if strings.HasPrefix(ref, git.TAG_PREFIX) { + ref = strings.TrimPrefix(ref, git.TAG_PREFIX) + refType = 2 + } else { + if gitRepo.IsBranchExist(ref) { + refType = 1 + } else if gitRepo.IsTagExist(ref) { + refType = 2 + } else { + c.NotFound() + return + } + } + + var sha string + if refType == 1 { + sha, err = gitRepo.GetBranchCommitID(ref) + } else if refType == 2 { + sha, err = gitRepo.GetTagCommitID(ref) + } + if err != nil { + c.NotFoundOrServerError("get reference commit ID", git.IsErrNotExist, err) + return + } + c.PlainText(http.StatusOK, []byte(sha)) +} diff --git a/internal/route/api/v1/repo/file.go b/internal/route/api/v1/repo/file.go new file mode 100644 index 00000000..4dcae313 --- /dev/null +++ b/internal/route/api/v1/repo/file.go @@ -0,0 +1,62 @@ +// Copyright 2014 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 ( + "github.com/gogs/git-module" + repo2 "gogs.io/gogs/internal/route/repo" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" +) + +func GetRawFile(c *context.APIContext) { + if !c.Repo.HasAccess() { + c.NotFound() + return + } + + if c.Repo.Repository.IsBare { + c.NotFound() + return + } + + blob, err := c.Repo.Commit.GetBlobByPath(c.Repo.TreePath) + if err != nil { + c.NotFoundOrServerError("GetBlobByPath", git.IsErrNotExist, err) + return + } + if err = repo2.ServeBlob(c.Context, blob); err != nil { + c.ServerError("ServeBlob", err) + } +} + +func GetArchive(c *context.APIContext) { + repoPath := db.RepoPath(c.Params(":username"), c.Params(":reponame")) + gitRepo, err := git.OpenRepository(repoPath) + if err != nil { + c.ServerError("OpenRepository", err) + return + } + c.Repo.GitRepo = gitRepo + + repo2.Download(c.Context) +} + +func GetEditorconfig(c *context.APIContext) { + ec, err := c.Repo.GetEditorconfig() + if err != nil { + c.NotFoundOrServerError("GetEditorconfig", git.IsErrNotExist, err) + return + } + + fileName := c.Params("filename") + def := ec.GetDefinitionForFilename(fileName) + if def == nil { + c.NotFound() + return + } + c.JSONSuccess(def) +} diff --git a/internal/route/api/v1/repo/hook.go b/internal/route/api/v1/repo/hook.go new file mode 100644 index 00000000..060d2049 --- /dev/null +++ b/internal/route/api/v1/repo/hook.go @@ -0,0 +1,185 @@ +// Copyright 2014 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 ( + "github.com/json-iterator/go" + "github.com/unknwon/com" + convert2 "gogs.io/gogs/internal/route/api/v1/convert" + + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/db/errors" +) + +// https://github.com/gogs/go-gogs-client/wiki/Repositories#list-hooks +func ListHooks(c *context.APIContext) { + hooks, err := db.GetWebhooksByRepoID(c.Repo.Repository.ID) + if err != nil { + c.Error(500, "GetWebhooksByRepoID", err) + return + } + + apiHooks := make([]*api.Hook, len(hooks)) + for i := range hooks { + apiHooks[i] = convert2.ToHook(c.Repo.RepoLink, hooks[i]) + } + c.JSON(200, &apiHooks) +} + +// https://github.com/gogs/go-gogs-client/wiki/Repositories#create-a-hook +func CreateHook(c *context.APIContext, form api.CreateHookOption) { + if !db.IsValidHookTaskType(form.Type) { + c.Error(422, "", "Invalid hook type") + return + } + for _, name := range []string{"url", "content_type"} { + if _, ok := form.Config[name]; !ok { + c.Error(422, "", "Missing config option: "+name) + return + } + } + if !db.IsValidHookContentType(form.Config["content_type"]) { + c.Error(422, "", "Invalid content type") + return + } + + if len(form.Events) == 0 { + form.Events = []string{"push"} + } + w := &db.Webhook{ + RepoID: c.Repo.Repository.ID, + URL: form.Config["url"], + ContentType: db.ToHookContentType(form.Config["content_type"]), + Secret: form.Config["secret"], + HookEvent: &db.HookEvent{ + ChooseEvents: true, + HookEvents: db.HookEvents{ + Create: com.IsSliceContainsStr(form.Events, string(db.HOOK_EVENT_CREATE)), + Delete: com.IsSliceContainsStr(form.Events, string(db.HOOK_EVENT_DELETE)), + Fork: com.IsSliceContainsStr(form.Events, string(db.HOOK_EVENT_FORK)), + Push: com.IsSliceContainsStr(form.Events, string(db.HOOK_EVENT_PUSH)), + Issues: com.IsSliceContainsStr(form.Events, string(db.HOOK_EVENT_ISSUES)), + IssueComment: com.IsSliceContainsStr(form.Events, string(db.HOOK_EVENT_ISSUE_COMMENT)), + PullRequest: com.IsSliceContainsStr(form.Events, string(db.HOOK_EVENT_PULL_REQUEST)), + Release: com.IsSliceContainsStr(form.Events, string(db.HOOK_EVENT_RELEASE)), + }, + }, + IsActive: form.Active, + HookTaskType: db.ToHookTaskType(form.Type), + } + if w.HookTaskType == db.SLACK { + channel, ok := form.Config["channel"] + if !ok { + c.Error(422, "", "Missing config option: channel") + return + } + meta, err := jsoniter.Marshal(&db.SlackMeta{ + Channel: channel, + Username: form.Config["username"], + IconURL: form.Config["icon_url"], + Color: form.Config["color"], + }) + if err != nil { + c.Error(500, "slack: JSON marshal failed", err) + return + } + w.Meta = string(meta) + } + + if err := w.UpdateEvent(); err != nil { + c.Error(500, "UpdateEvent", err) + return + } else if err := db.CreateWebhook(w); err != nil { + c.Error(500, "CreateWebhook", err) + return + } + + c.JSON(201, convert2.ToHook(c.Repo.RepoLink, w)) +} + +// https://github.com/gogs/go-gogs-client/wiki/Repositories#edit-a-hook +func EditHook(c *context.APIContext, form api.EditHookOption) { + w, err := db.GetWebhookOfRepoByID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + if errors.IsWebhookNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetWebhookOfRepoByID", err) + } + return + } + + if form.Config != nil { + if url, ok := form.Config["url"]; ok { + w.URL = url + } + if ct, ok := form.Config["content_type"]; ok { + if !db.IsValidHookContentType(ct) { + c.Error(422, "", "Invalid content type") + return + } + w.ContentType = db.ToHookContentType(ct) + } + + if w.HookTaskType == db.SLACK { + if channel, ok := form.Config["channel"]; ok { + meta, err := jsoniter.Marshal(&db.SlackMeta{ + Channel: channel, + Username: form.Config["username"], + IconURL: form.Config["icon_url"], + Color: form.Config["color"], + }) + if err != nil { + c.Error(500, "slack: JSON marshal failed", err) + return + } + w.Meta = string(meta) + } + } + } + + // Update events + if len(form.Events) == 0 { + form.Events = []string{"push"} + } + w.PushOnly = false + w.SendEverything = false + w.ChooseEvents = true + w.Create = com.IsSliceContainsStr(form.Events, string(db.HOOK_EVENT_CREATE)) + w.Delete = com.IsSliceContainsStr(form.Events, string(db.HOOK_EVENT_DELETE)) + w.Fork = com.IsSliceContainsStr(form.Events, string(db.HOOK_EVENT_FORK)) + w.Push = com.IsSliceContainsStr(form.Events, string(db.HOOK_EVENT_PUSH)) + w.Issues = com.IsSliceContainsStr(form.Events, string(db.HOOK_EVENT_ISSUES)) + w.IssueComment = com.IsSliceContainsStr(form.Events, string(db.HOOK_EVENT_ISSUE_COMMENT)) + w.PullRequest = com.IsSliceContainsStr(form.Events, string(db.HOOK_EVENT_PULL_REQUEST)) + w.Release = com.IsSliceContainsStr(form.Events, string(db.HOOK_EVENT_RELEASE)) + if err = w.UpdateEvent(); err != nil { + c.Error(500, "UpdateEvent", err) + return + } + + if form.Active != nil { + w.IsActive = *form.Active + } + + if err := db.UpdateWebhook(w); err != nil { + c.Error(500, "UpdateWebhook", err) + return + } + + c.JSON(200, convert2.ToHook(c.Repo.RepoLink, w)) +} + +func DeleteHook(c *context.APIContext) { + if err := db.DeleteWebhookOfRepoByID(c.Repo.Repository.ID, c.ParamsInt64(":id")); err != nil { + c.Error(500, "DeleteWebhookByRepoID", err) + return + } + + c.Status(204) +} diff --git a/internal/route/api/v1/repo/issue.go b/internal/route/api/v1/repo/issue.go new file mode 100644 index 00000000..5d32a00c --- /dev/null +++ b/internal/route/api/v1/repo/issue.go @@ -0,0 +1,194 @@ +// 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" + "net/http" + "strings" + + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/db/errors" + "gogs.io/gogs/internal/setting" +) + +func listIssues(c *context.APIContext, opts *db.IssuesOptions) { + issues, err := db.Issues(opts) + if err != nil { + c.ServerError("Issues", err) + return + } + + count, err := db.IssuesCount(opts) + if err != nil { + c.ServerError("IssuesCount", err) + return + } + + // FIXME: use IssueList to improve performance. + apiIssues := make([]*api.Issue, len(issues)) + for i := range issues { + if err = issues[i].LoadAttributes(); err != nil { + c.ServerError("LoadAttributes", err) + return + } + apiIssues[i] = issues[i].APIFormat() + } + + c.SetLinkHeader(int(count), setting.UI.IssuePagingNum) + c.JSONSuccess(&apiIssues) +} + +func ListUserIssues(c *context.APIContext) { + opts := db.IssuesOptions{ + AssigneeID: c.User.ID, + Page: c.QueryInt("page"), + IsClosed: api.StateType(c.Query("state")) == api.STATE_CLOSED, + } + + listIssues(c, &opts) +} + +func ListIssues(c *context.APIContext) { + opts := db.IssuesOptions{ + RepoID: c.Repo.Repository.ID, + Page: c.QueryInt("page"), + IsClosed: api.StateType(c.Query("state")) == api.STATE_CLOSED, + } + + listIssues(c, &opts) +} + +func GetIssue(c *context.APIContext) { + issue, err := db.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + c.NotFoundOrServerError("GetIssueByIndex", errors.IsIssueNotExist, err) + return + } + c.JSONSuccess(issue.APIFormat()) +} + +func CreateIssue(c *context.APIContext, form api.CreateIssueOption) { + issue := &db.Issue{ + RepoID: c.Repo.Repository.ID, + Title: form.Title, + PosterID: c.User.ID, + Poster: c.User, + Content: form.Body, + } + + if c.Repo.IsWriter() { + if len(form.Assignee) > 0 { + assignee, err := db.GetUserByName(form.Assignee) + if err != nil { + if errors.IsUserNotExist(err) { + c.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("assignee does not exist: [name: %s]", form.Assignee)) + } else { + c.ServerError("GetUserByName", err) + } + return + } + issue.AssigneeID = assignee.ID + } + issue.MilestoneID = form.Milestone + } else { + form.Labels = nil + } + + if err := db.NewIssue(c.Repo.Repository, issue, form.Labels, nil); err != nil { + c.ServerError("NewIssue", err) + return + } + + if form.Closed { + if err := issue.ChangeStatus(c.User, c.Repo.Repository, true); err != nil { + c.ServerError("ChangeStatus", err) + return + } + } + + // Refetch from database to assign some automatic values + var err error + issue, err = db.GetIssueByID(issue.ID) + if err != nil { + c.ServerError("GetIssueByID", err) + return + } + c.JSON(http.StatusCreated, issue.APIFormat()) +} + +func EditIssue(c *context.APIContext, form api.EditIssueOption) { + issue, err := db.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + c.NotFoundOrServerError("GetIssueByIndex", errors.IsIssueNotExist, err) + return + } + + if !issue.IsPoster(c.User.ID) && !c.Repo.IsWriter() { + c.Status(http.StatusForbidden) + return + } + + if len(form.Title) > 0 { + issue.Title = form.Title + } + if form.Body != nil { + issue.Content = *form.Body + } + + if c.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 := db.GetUserByName(*form.Assignee) + if err != nil { + if errors.IsUserNotExist(err) { + c.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("assignee does not exist: [name: %s]", *form.Assignee)) + } else { + c.ServerError("GetUserByName", err) + } + return + } + issue.AssigneeID = assignee.ID + } + + if err = db.UpdateIssueUserByAssignee(issue); err != nil { + c.ServerError("UpdateIssueUserByAssignee", err) + return + } + } + if c.Repo.IsWriter() && form.Milestone != nil && + issue.MilestoneID != *form.Milestone { + oldMilestoneID := issue.MilestoneID + issue.MilestoneID = *form.Milestone + if err = db.ChangeMilestoneAssign(c.User, issue, oldMilestoneID); err != nil { + c.ServerError("ChangeMilestoneAssign", err) + return + } + } + + if err = db.UpdateIssue(issue); err != nil { + c.ServerError("UpdateIssue", err) + return + } + if form.State != nil { + if err = issue.ChangeStatus(c.User, c.Repo.Repository, api.STATE_CLOSED == api.StateType(*form.State)); err != nil { + c.ServerError("ChangeStatus", err) + return + } + } + + // Refetch from database to assign some automatic values + issue, err = db.GetIssueByID(issue.ID) + if err != nil { + c.ServerError("GetIssueByID", err) + return + } + c.JSON(http.StatusCreated, issue.APIFormat()) +} diff --git a/internal/route/api/v1/repo/issue_comment.go b/internal/route/api/v1/repo/issue_comment.go new file mode 100644 index 00000000..4f86e13b --- /dev/null +++ b/internal/route/api/v1/repo/issue_comment.go @@ -0,0 +1,131 @@ +// Copyright 2015 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 ( + "net/http" + "time" + + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" +) + +func ListIssueComments(c *context.APIContext) { + var since time.Time + if len(c.Query("since")) > 0 { + var err error + since, err = time.Parse(time.RFC3339, c.Query("since")) + if err != nil { + c.Error(http.StatusUnprocessableEntity, "", err) + return + } + } + + // comments,err:=db.GetCommentsByIssueIDSince(, since) + issue, err := db.GetRawIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + c.ServerError("GetRawIssueByIndex", err) + return + } + + comments, err := db.GetCommentsByIssueIDSince(issue.ID, since.Unix()) + if err != nil { + c.ServerError("GetCommentsByIssueIDSince", err) + return + } + + apiComments := make([]*api.Comment, len(comments)) + for i := range comments { + apiComments[i] = comments[i].APIFormat() + } + c.JSONSuccess(&apiComments) +} + +func ListRepoIssueComments(c *context.APIContext) { + var since time.Time + if len(c.Query("since")) > 0 { + var err error + since, err = time.Parse(time.RFC3339, c.Query("since")) + if err != nil { + c.Error(http.StatusUnprocessableEntity, "", err) + return + } + } + + comments, err := db.GetCommentsByRepoIDSince(c.Repo.Repository.ID, since.Unix()) + if err != nil { + c.ServerError("GetCommentsByRepoIDSince", err) + return + } + + apiComments := make([]*api.Comment, len(comments)) + for i := range comments { + apiComments[i] = comments[i].APIFormat() + } + c.JSONSuccess(&apiComments) +} + +func CreateIssueComment(c *context.APIContext, form api.CreateIssueCommentOption) { + issue, err := db.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + c.ServerError("GetIssueByIndex", err) + return + } + + comment, err := db.CreateIssueComment(c.User, c.Repo.Repository, issue, form.Body, nil) + if err != nil { + c.ServerError("CreateIssueComment", err) + return + } + + c.JSON(http.StatusCreated, comment.APIFormat()) +} + +func EditIssueComment(c *context.APIContext, form api.EditIssueCommentOption) { + comment, err := db.GetCommentByID(c.ParamsInt64(":id")) + if err != nil { + c.NotFoundOrServerError("GetCommentByID", db.IsErrCommentNotExist, err) + return + } + + if c.User.ID != comment.PosterID && !c.Repo.IsAdmin() { + c.Status(http.StatusForbidden) + return + } else if comment.Type != db.COMMENT_TYPE_COMMENT { + c.NoContent() + return + } + + oldContent := comment.Content + comment.Content = form.Body + if err := db.UpdateComment(c.User, comment, oldContent); err != nil { + c.ServerError("UpdateComment", err) + return + } + c.JSONSuccess(comment.APIFormat()) +} + +func DeleteIssueComment(c *context.APIContext) { + comment, err := db.GetCommentByID(c.ParamsInt64(":id")) + if err != nil { + c.NotFoundOrServerError("GetCommentByID", db.IsErrCommentNotExist, err) + return + } + + if c.User.ID != comment.PosterID && !c.Repo.IsAdmin() { + c.Status(http.StatusForbidden) + return + } else if comment.Type != db.COMMENT_TYPE_COMMENT { + c.NoContent() + return + } + + if err = db.DeleteCommentByID(c.User, comment.ID); err != nil { + c.ServerError("DeleteCommentByID", err) + return + } + c.NoContent() +} diff --git a/internal/route/api/v1/repo/issue_label.go b/internal/route/api/v1/repo/issue_label.go new file mode 100644 index 00000000..7c8b7982 --- /dev/null +++ b/internal/route/api/v1/repo/issue_label.go @@ -0,0 +1,131 @@ +// 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 ( + "net/http" + + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/db/errors" +) + +func ListIssueLabels(c *context.APIContext) { + issue, err := db.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + c.NotFoundOrServerError("GetIssueByIndex", errors.IsIssueNotExist, err) + return + } + + apiLabels := make([]*api.Label, len(issue.Labels)) + for i := range issue.Labels { + apiLabels[i] = issue.Labels[i].APIFormat() + } + c.JSONSuccess(&apiLabels) +} + +func AddIssueLabels(c *context.APIContext, form api.IssueLabelsOption) { + issue, err := db.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + c.NotFoundOrServerError("GetIssueByIndex", errors.IsIssueNotExist, err) + return + } + + labels, err := db.GetLabelsInRepoByIDs(c.Repo.Repository.ID, form.Labels) + if err != nil { + c.ServerError("GetLabelsInRepoByIDs", err) + return + } + + if err = issue.AddLabels(c.User, labels); err != nil { + c.ServerError("AddLabels", err) + return + } + + labels, err = db.GetLabelsByIssueID(issue.ID) + if err != nil { + c.ServerError("GetLabelsByIssueID", err) + return + } + + apiLabels := make([]*api.Label, len(labels)) + for i := range labels { + apiLabels[i] = issue.Labels[i].APIFormat() + } + c.JSONSuccess(&apiLabels) +} + +func DeleteIssueLabel(c *context.APIContext) { + issue, err := db.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + c.NotFoundOrServerError("GetIssueByIndex", errors.IsIssueNotExist, err) + return + } + + label, err := db.GetLabelOfRepoByID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + if db.IsErrLabelNotExist(err) { + c.Error(http.StatusUnprocessableEntity, "", err) + } else { + c.ServerError("GetLabelInRepoByID", err) + } + return + } + + if err := db.DeleteIssueLabel(issue, label); err != nil { + c.ServerError("DeleteIssueLabel", err) + return + } + + c.NoContent() +} + +func ReplaceIssueLabels(c *context.APIContext, form api.IssueLabelsOption) { + issue, err := db.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + c.NotFoundOrServerError("GetIssueByIndex", errors.IsIssueNotExist, err) + return + } + + labels, err := db.GetLabelsInRepoByIDs(c.Repo.Repository.ID, form.Labels) + if err != nil { + c.ServerError("GetLabelsInRepoByIDs", err) + return + } + + if err := issue.ReplaceLabels(labels); err != nil { + c.ServerError("ReplaceLabels", err) + return + } + + labels, err = db.GetLabelsByIssueID(issue.ID) + if err != nil { + c.ServerError("GetLabelsByIssueID", err) + return + } + + apiLabels := make([]*api.Label, len(labels)) + for i := range labels { + apiLabels[i] = issue.Labels[i].APIFormat() + } + c.JSONSuccess(&apiLabels) +} + +func ClearIssueLabels(c *context.APIContext) { + issue, err := db.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + c.NotFoundOrServerError("GetIssueByIndex", errors.IsIssueNotExist, err) + return + } + + if err := issue.ClearLabels(c.User); err != nil { + c.ServerError("ClearLabels", err) + return + } + + c.NoContent() +} diff --git a/internal/route/api/v1/repo/key.go b/internal/route/api/v1/repo/key.go new file mode 100644 index 00000000..d47d4b46 --- /dev/null +++ b/internal/route/api/v1/repo/key.go @@ -0,0 +1,114 @@ +// Copyright 2015 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" + convert2 "gogs.io/gogs/internal/route/api/v1/convert" + + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/setting" +) + +func composeDeployKeysAPILink(repoPath string) string { + return setting.AppURL + "api/v1/repos/" + repoPath + "/keys/" +} + +// https://github.com/gogs/go-gogs-client/wiki/Repositories-Deploy-Keys#list-deploy-keys +func ListDeployKeys(c *context.APIContext) { + keys, err := db.ListDeployKeys(c.Repo.Repository.ID) + if err != nil { + c.Error(500, "ListDeployKeys", err) + return + } + + apiLink := composeDeployKeysAPILink(c.Repo.Owner.Name + "/" + c.Repo.Repository.Name) + apiKeys := make([]*api.DeployKey, len(keys)) + for i := range keys { + if err = keys[i].GetContent(); err != nil { + c.Error(500, "GetContent", err) + return + } + apiKeys[i] = convert2.ToDeployKey(apiLink, keys[i]) + } + + c.JSON(200, &apiKeys) +} + +// https://github.com/gogs/go-gogs-client/wiki/Repositories-Deploy-Keys#get-a-deploy-key +func GetDeployKey(c *context.APIContext) { + key, err := db.GetDeployKeyByID(c.ParamsInt64(":id")) + if err != nil { + if db.IsErrDeployKeyNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetDeployKeyByID", err) + } + return + } + + if err = key.GetContent(); err != nil { + c.Error(500, "GetContent", err) + return + } + + apiLink := composeDeployKeysAPILink(c.Repo.Owner.Name + "/" + c.Repo.Repository.Name) + c.JSON(200, convert2.ToDeployKey(apiLink, key)) +} + +func HandleCheckKeyStringError(c *context.APIContext, err error) { + if db.IsErrKeyUnableVerify(err) { + c.Error(422, "", "Unable to verify key content") + } else { + c.Error(422, "", fmt.Errorf("Invalid key content: %v", err)) + } +} + +func HandleAddKeyError(c *context.APIContext, err error) { + switch { + case db.IsErrKeyAlreadyExist(err): + c.Error(422, "", "Key content has been used as non-deploy key") + case db.IsErrKeyNameAlreadyUsed(err): + c.Error(422, "", "Key title has been used") + default: + c.Error(500, "AddKey", err) + } +} + +// https://github.com/gogs/go-gogs-client/wiki/Repositories-Deploy-Keys#add-a-new-deploy-key +func CreateDeployKey(c *context.APIContext, form api.CreateKeyOption) { + content, err := db.CheckPublicKeyString(form.Key) + if err != nil { + HandleCheckKeyStringError(c, err) + return + } + + key, err := db.AddDeployKey(c.Repo.Repository.ID, form.Title, content) + if err != nil { + HandleAddKeyError(c, err) + return + } + + key.Content = content + apiLink := composeDeployKeysAPILink(c.Repo.Owner.Name + "/" + c.Repo.Repository.Name) + c.JSON(201, convert2.ToDeployKey(apiLink, key)) +} + +// https://github.com/gogs/go-gogs-client/wiki/Repositories-Deploy-Keys#remove-a-deploy-key +func DeleteDeploykey(c *context.APIContext) { + if err := db.DeleteDeployKey(c.User, c.ParamsInt64(":id")); err != nil { + if db.IsErrKeyAccessDenied(err) { + c.Error(403, "", "You do not have access to this key") + } else { + c.Error(500, "DeleteDeployKey", err) + } + return + } + + c.Status(204) +} diff --git a/internal/route/api/v1/repo/label.go b/internal/route/api/v1/repo/label.go new file mode 100644 index 00000000..9dd2d7d0 --- /dev/null +++ b/internal/route/api/v1/repo/label.go @@ -0,0 +1,89 @@ +// 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 ( + "net/http" + + "github.com/unknwon/com" + + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" +) + +func ListLabels(c *context.APIContext) { + labels, err := db.GetLabelsByRepoID(c.Repo.Repository.ID) + if err != nil { + c.ServerError("GetLabelsByRepoID", err) + return + } + + apiLabels := make([]*api.Label, len(labels)) + for i := range labels { + apiLabels[i] = labels[i].APIFormat() + } + c.JSONSuccess(&apiLabels) +} + +func GetLabel(c *context.APIContext) { + var label *db.Label + var err error + idStr := c.Params(":id") + if id := com.StrTo(idStr).MustInt64(); id > 0 { + label, err = db.GetLabelOfRepoByID(c.Repo.Repository.ID, id) + } else { + label, err = db.GetLabelOfRepoByName(c.Repo.Repository.ID, idStr) + } + if err != nil { + c.NotFoundOrServerError("GetLabel", db.IsErrLabelNotExist, err) + return + } + + c.JSONSuccess(label.APIFormat()) +} + +func CreateLabel(c *context.APIContext, form api.CreateLabelOption) { + label := &db.Label{ + Name: form.Name, + Color: form.Color, + RepoID: c.Repo.Repository.ID, + } + if err := db.NewLabels(label); err != nil { + c.ServerError("NewLabel", err) + return + } + c.JSON(http.StatusCreated, label.APIFormat()) +} + +func EditLabel(c *context.APIContext, form api.EditLabelOption) { + label, err := db.GetLabelOfRepoByID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + c.NotFoundOrServerError("GetLabelOfRepoByID", db.IsErrLabelNotExist, err) + return + } + + if form.Name != nil { + label.Name = *form.Name + } + if form.Color != nil { + label.Color = *form.Color + } + if err := db.UpdateLabel(label); err != nil { + c.ServerError("UpdateLabel", err) + return + } + c.JSONSuccess(label.APIFormat()) +} + +func DeleteLabel(c *context.APIContext) { + if err := db.DeleteLabel(c.Repo.Repository.ID, c.ParamsInt64(":id")); err != nil { + c.ServerError("DeleteLabel", err) + return + } + + c.NoContent() +} diff --git a/internal/route/api/v1/repo/milestone.go b/internal/route/api/v1/repo/milestone.go new file mode 100644 index 00000000..6f5fea17 --- /dev/null +++ b/internal/route/api/v1/repo/milestone.go @@ -0,0 +1,96 @@ +// 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 ( + "net/http" + "time" + + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" +) + +func ListMilestones(c *context.APIContext) { + milestones, err := db.GetMilestonesByRepoID(c.Repo.Repository.ID) + if err != nil { + c.ServerError("GetMilestonesByRepoID", err) + return + } + + apiMilestones := make([]*api.Milestone, len(milestones)) + for i := range milestones { + apiMilestones[i] = milestones[i].APIFormat() + } + c.JSONSuccess(&apiMilestones) +} + +func GetMilestone(c *context.APIContext) { + milestone, err := db.GetMilestoneByRepoID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + c.NotFoundOrServerError("GetMilestoneByRepoID", db.IsErrMilestoneNotExist, err) + return + } + c.JSONSuccess(milestone.APIFormat()) +} + +func CreateMilestone(c *context.APIContext, form api.CreateMilestoneOption) { + if form.Deadline == nil { + defaultDeadline, _ := time.ParseInLocation("2006-01-02", "9999-12-31", time.Local) + form.Deadline = &defaultDeadline + } + + milestone := &db.Milestone{ + RepoID: c.Repo.Repository.ID, + Name: form.Title, + Content: form.Description, + Deadline: *form.Deadline, + } + + if err := db.NewMilestone(milestone); err != nil { + c.ServerError("NewMilestone", err) + return + } + c.JSON(http.StatusCreated, milestone.APIFormat()) +} + +func EditMilestone(c *context.APIContext, form api.EditMilestoneOption) { + milestone, err := db.GetMilestoneByRepoID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + c.NotFoundOrServerError("GetMilestoneByRepoID", db.IsErrMilestoneNotExist, err) + return + } + + if len(form.Title) > 0 { + milestone.Name = form.Title + } + if form.Description != nil { + milestone.Content = *form.Description + } + if form.Deadline != nil && !form.Deadline.IsZero() { + milestone.Deadline = *form.Deadline + } + + if form.State != nil { + if err = milestone.ChangeStatus(api.STATE_CLOSED == api.StateType(*form.State)); err != nil { + c.ServerError("ChangeStatus", err) + return + } + } else if err = db.UpdateMilestone(milestone); err != nil { + c.ServerError("UpdateMilestone", err) + return + } + + c.JSONSuccess(milestone.APIFormat()) +} + +func DeleteMilestone(c *context.APIContext) { + if err := db.DeleteMilestoneOfRepoByID(c.Repo.Repository.ID, c.ParamsInt64(":id")); err != nil { + c.ServerError("DeleteMilestoneByRepoID", err) + return + } + c.NoContent() +} diff --git a/internal/route/api/v1/repo/repo.go b/internal/route/api/v1/repo/repo.go new file mode 100644 index 00000000..096096fb --- /dev/null +++ b/internal/route/api/v1/repo/repo.go @@ -0,0 +1,407 @@ +// Copyright 2014 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" + convert2 "gogs.io/gogs/internal/route/api/v1/convert" + "net/http" + "path" + + log "gopkg.in/clog.v1" + + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/db/errors" + "gogs.io/gogs/internal/form" + "gogs.io/gogs/internal/setting" +) + +func Search(c *context.APIContext) { + opts := &db.SearchRepoOptions{ + Keyword: path.Base(c.Query("q")), + OwnerID: c.QueryInt64("uid"), + PageSize: convert2.ToCorrectPageSize(c.QueryInt("limit")), + Page: c.QueryInt("page"), + } + + // Check visibility. + if c.IsLogged && opts.OwnerID > 0 { + if c.User.ID == opts.OwnerID { + opts.Private = true + } else { + u, err := db.GetUserByID(opts.OwnerID) + if err != nil { + c.JSON(http.StatusInternalServerError, map[string]interface{}{ + "ok": false, + "error": err.Error(), + }) + return + } + if u.IsOrganization() && u.IsOwnedBy(c.User.ID) { + opts.Private = true + } + // FIXME: how about collaborators? + } + } + + repos, count, err := db.SearchRepositoryByName(opts) + if err != nil { + c.JSON(http.StatusInternalServerError, map[string]interface{}{ + "ok": false, + "error": err.Error(), + }) + return + } + + if err = db.RepositoryList(repos).LoadAttributes(); err != nil { + c.JSON(http.StatusInternalServerError, map[string]interface{}{ + "ok": false, + "error": err.Error(), + }) + return + } + + results := make([]*api.Repository, len(repos)) + for i := range repos { + results[i] = repos[i].APIFormat(nil) + } + + c.SetLinkHeader(int(count), opts.PageSize) + c.JSONSuccess(map[string]interface{}{ + "ok": true, + "data": results, + }) +} + +func listUserRepositories(c *context.APIContext, username string) { + user, err := db.GetUserByName(username) + if err != nil { + c.NotFoundOrServerError("GetUserByName", errors.IsUserNotExist, err) + return + } + + // Only list public repositories if user requests someone else's repository list, + // or an organization isn't a member of. + var ownRepos []*db.Repository + if user.IsOrganization() { + ownRepos, _, err = user.GetUserRepositories(c.User.ID, 1, user.NumRepos) + } else { + ownRepos, err = db.GetUserRepositories(&db.UserRepoOptions{ + UserID: user.ID, + Private: c.User.ID == user.ID, + Page: 1, + PageSize: user.NumRepos, + }) + } + if err != nil { + c.ServerError("GetUserRepositories", err) + return + } + + if err = db.RepositoryList(ownRepos).LoadAttributes(); err != nil { + c.ServerError("LoadAttributes(ownRepos)", err) + return + } + + // Early return for querying other user's repositories + if c.User.ID != user.ID { + repos := make([]*api.Repository, len(ownRepos)) + for i := range ownRepos { + repos[i] = ownRepos[i].APIFormat(&api.Permission{true, true, true}) + } + c.JSONSuccess(&repos) + return + } + + accessibleRepos, err := user.GetRepositoryAccesses() + if err != nil { + c.ServerError("GetRepositoryAccesses", err) + return + } + + numOwnRepos := len(ownRepos) + repos := make([]*api.Repository, numOwnRepos+len(accessibleRepos)) + for i := range ownRepos { + repos[i] = ownRepos[i].APIFormat(&api.Permission{true, true, true}) + } + + i := numOwnRepos + for repo, access := range accessibleRepos { + repos[i] = repo.APIFormat(&api.Permission{ + Admin: access >= db.ACCESS_MODE_ADMIN, + Push: access >= db.ACCESS_MODE_WRITE, + Pull: true, + }) + i++ + } + + c.JSONSuccess(&repos) +} + +func ListMyRepos(c *context.APIContext) { + listUserRepositories(c, c.User.Name) +} + +func ListUserRepositories(c *context.APIContext) { + listUserRepositories(c, c.Params(":username")) +} + +func ListOrgRepositories(c *context.APIContext) { + listUserRepositories(c, c.Params(":org")) +} + +func CreateUserRepo(c *context.APIContext, owner *db.User, opt api.CreateRepoOption) { + repo, err := db.CreateRepository(c.User, owner, db.CreateRepoOptions{ + Name: opt.Name, + Description: opt.Description, + Gitignores: opt.Gitignores, + License: opt.License, + Readme: opt.Readme, + IsPrivate: opt.Private, + AutoInit: opt.AutoInit, + }) + if err != nil { + if db.IsErrRepoAlreadyExist(err) || + db.IsErrNameReserved(err) || + db.IsErrNamePatternNotAllowed(err) { + c.Error(http.StatusUnprocessableEntity, "", err) + } else { + if repo != nil { + if err = db.DeleteRepository(c.User.ID, repo.ID); err != nil { + log.Error(2, "DeleteRepository: %v", err) + } + } + c.ServerError("CreateRepository", err) + } + return + } + + c.JSON(201, repo.APIFormat(&api.Permission{true, true, true})) +} + +func Create(c *context.APIContext, opt api.CreateRepoOption) { + // Shouldn't reach this condition, but just in case. + if c.User.IsOrganization() { + c.Error(http.StatusUnprocessableEntity, "", "not allowed creating repository for organization") + return + } + CreateUserRepo(c, c.User, opt) +} + +func CreateOrgRepo(c *context.APIContext, opt api.CreateRepoOption) { + org, err := db.GetOrgByName(c.Params(":org")) + if err != nil { + c.NotFoundOrServerError("GetOrgByName", errors.IsUserNotExist, err) + return + } + + if !org.IsOwnedBy(c.User.ID) { + c.Error(http.StatusForbidden, "", "given user is not owner of organization") + return + } + CreateUserRepo(c, org, opt) +} + +func Migrate(c *context.APIContext, f form.MigrateRepo) { + ctxUser := c.User + // Not equal means context user is an organization, + // or is another user/organization if current user is admin. + if f.Uid != ctxUser.ID { + org, err := db.GetUserByID(f.Uid) + if err != nil { + if errors.IsUserNotExist(err) { + c.Error(http.StatusUnprocessableEntity, "", err) + } else { + c.Error(http.StatusInternalServerError, "GetUserByID", err) + } + return + } else if !org.IsOrganization() && !c.User.IsAdmin { + c.Error(http.StatusForbidden, "", "given user is not an organization") + return + } + ctxUser = org + } + + if c.HasError() { + c.Error(http.StatusUnprocessableEntity, "", c.GetErrMsg()) + return + } + + if ctxUser.IsOrganization() && !c.User.IsAdmin { + // Check ownership of organization. + if !ctxUser.IsOwnedBy(c.User.ID) { + c.Error(http.StatusForbidden, "", "Given user is not owner of organization") + return + } + } + + remoteAddr, err := f.ParseRemoteAddr(c.User) + if err != nil { + if db.IsErrInvalidCloneAddr(err) { + addrErr := err.(db.ErrInvalidCloneAddr) + switch { + case addrErr.IsURLError: + c.Error(http.StatusUnprocessableEntity, "", err) + case addrErr.IsPermissionDenied: + c.Error(http.StatusUnprocessableEntity, "", "you are not allowed to import local repositories") + case addrErr.IsInvalidPath: + c.Error(http.StatusUnprocessableEntity, "", "invalid local path, it does not exist or not a directory") + default: + c.ServerError("ParseRemoteAddr", fmt.Errorf("unknown error type (ErrInvalidCloneAddr): %v", err)) + } + } else { + c.ServerError("ParseRemoteAddr", err) + } + return + } + + repo, err := db.MigrateRepository(c.User, ctxUser, db.MigrateRepoOptions{ + Name: f.RepoName, + Description: f.Description, + IsPrivate: f.Private || setting.Repository.ForcePrivate, + IsMirror: f.Mirror, + RemoteAddr: remoteAddr, + }) + if err != nil { + if repo != nil { + if errDelete := db.DeleteRepository(ctxUser.ID, repo.ID); errDelete != nil { + log.Error(2, "DeleteRepository: %v", errDelete) + } + } + + if errors.IsReachLimitOfRepo(err) { + c.Error(http.StatusUnprocessableEntity, "", err) + } else { + c.ServerError("MigrateRepository", errors.New(db.HandleMirrorCredentials(err.Error(), true))) + } + return + } + + log.Trace("Repository migrated: %s/%s", ctxUser.Name, f.RepoName) + c.JSON(201, repo.APIFormat(&api.Permission{true, true, true})) +} + +// FIXME: inject in the handler chain +func parseOwnerAndRepo(c *context.APIContext) (*db.User, *db.Repository) { + owner, err := db.GetUserByName(c.Params(":username")) + if err != nil { + if errors.IsUserNotExist(err) { + c.Error(http.StatusUnprocessableEntity, "", err) + } else { + c.ServerError("GetUserByName", err) + } + return nil, nil + } + + repo, err := db.GetRepositoryByName(owner.ID, c.Params(":reponame")) + if err != nil { + c.NotFoundOrServerError("GetRepositoryByName", errors.IsRepoNotExist, err) + return nil, nil + } + + return owner, repo +} + +func Get(c *context.APIContext) { + _, repo := parseOwnerAndRepo(c) + if c.Written() { + return + } + + c.JSONSuccess(repo.APIFormat(&api.Permission{ + Admin: c.Repo.IsAdmin(), + Push: c.Repo.IsWriter(), + Pull: true, + })) +} + +func Delete(c *context.APIContext) { + owner, repo := parseOwnerAndRepo(c) + if c.Written() { + return + } + + if owner.IsOrganization() && !owner.IsOwnedBy(c.User.ID) { + c.Error(http.StatusForbidden, "", "given user is not owner of organization") + return + } + + if err := db.DeleteRepository(owner.ID, repo.ID); err != nil { + c.ServerError("DeleteRepository", err) + return + } + + log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name) + c.NoContent() +} + +func ListForks(c *context.APIContext) { + forks, err := c.Repo.Repository.GetForks() + if err != nil { + c.ServerError("GetForks", err) + return + } + + apiForks := make([]*api.Repository, len(forks)) + for i := range forks { + if err := forks[i].GetOwner(); err != nil { + c.ServerError("GetOwner", err) + return + } + apiForks[i] = forks[i].APIFormat(&api.Permission{ + Admin: c.User.IsAdminOfRepo(forks[i]), + Push: c.User.IsWriterOfRepo(forks[i]), + Pull: true, + }) + } + + c.JSONSuccess(&apiForks) +} + +func IssueTracker(c *context.APIContext, form api.EditIssueTrackerOption) { + _, repo := parseOwnerAndRepo(c) + if c.Written() { + return + } + + if form.EnableIssues != nil { + repo.EnableIssues = *form.EnableIssues + } + if form.EnableExternalTracker != nil { + repo.EnableExternalTracker = *form.EnableExternalTracker + } + if form.ExternalTrackerURL != nil { + repo.ExternalTrackerURL = *form.ExternalTrackerURL + } + if form.TrackerURLFormat != nil { + repo.ExternalTrackerFormat = *form.TrackerURLFormat + } + if form.TrackerIssueStyle != nil { + repo.ExternalTrackerStyle = *form.TrackerIssueStyle + } + + if err := db.UpdateRepository(repo, false); err != nil { + c.ServerError("UpdateRepository", err) + return + } + + c.NoContent() +} + +func MirrorSync(c *context.APIContext) { + _, repo := parseOwnerAndRepo(c) + if c.Written() { + return + } else if !repo.IsMirror { + c.NotFound() + return + } + + go db.MirrorQueue.Add(repo.ID) + c.Status(http.StatusAccepted) +} diff --git a/internal/route/api/v1/user/app.go b/internal/route/api/v1/user/app.go new file mode 100644 index 00000000..5db0175b --- /dev/null +++ b/internal/route/api/v1/user/app.go @@ -0,0 +1,45 @@ +// Copyright 2014 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 user + +import ( + "net/http" + + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/db/errors" +) + +func ListAccessTokens(c *context.APIContext) { + tokens, err := db.ListAccessTokens(c.User.ID) + if err != nil { + c.ServerError("ListAccessTokens", err) + return + } + + apiTokens := make([]*api.AccessToken, len(tokens)) + for i := range tokens { + apiTokens[i] = &api.AccessToken{tokens[i].Name, tokens[i].Sha1} + } + c.JSONSuccess(&apiTokens) +} + +func CreateAccessToken(c *context.APIContext, form api.CreateAccessTokenOption) { + t := &db.AccessToken{ + UID: c.User.ID, + Name: form.Name, + } + if err := db.NewAccessToken(t); err != nil { + if errors.IsAccessTokenNameAlreadyExist(err) { + c.Error(http.StatusUnprocessableEntity, "", err) + } else { + c.ServerError("NewAccessToken", err) + } + return + } + c.JSON(http.StatusCreated, &api.AccessToken{t.Name, t.Sha1}) +} diff --git a/internal/route/api/v1/user/email.go b/internal/route/api/v1/user/email.go new file mode 100644 index 00000000..e4baaefa --- /dev/null +++ b/internal/route/api/v1/user/email.go @@ -0,0 +1,81 @@ +// Copyright 2015 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 user + +import ( + convert2 "gogs.io/gogs/internal/route/api/v1/convert" + "net/http" + + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/setting" +) + +func ListEmails(c *context.APIContext) { + emails, err := db.GetEmailAddresses(c.User.ID) + if err != nil { + c.ServerError("GetEmailAddresses", err) + return + } + apiEmails := make([]*api.Email, len(emails)) + for i := range emails { + apiEmails[i] = convert2.ToEmail(emails[i]) + } + c.JSONSuccess(&apiEmails) +} + +func AddEmail(c *context.APIContext, form api.CreateEmailOption) { + if len(form.Emails) == 0 { + c.Status(http.StatusUnprocessableEntity) + return + } + + emails := make([]*db.EmailAddress, len(form.Emails)) + for i := range form.Emails { + emails[i] = &db.EmailAddress{ + UID: c.User.ID, + Email: form.Emails[i], + IsActivated: !setting.Service.RegisterEmailConfirm, + } + } + + if err := db.AddEmailAddresses(emails); err != nil { + if db.IsErrEmailAlreadyUsed(err) { + c.Error(http.StatusUnprocessableEntity, "", "email address has been used: "+err.(db.ErrEmailAlreadyUsed).Email) + } else { + c.Error(http.StatusInternalServerError, "AddEmailAddresses", err) + } + return + } + + apiEmails := make([]*api.Email, len(emails)) + for i := range emails { + apiEmails[i] = convert2.ToEmail(emails[i]) + } + c.JSON(http.StatusCreated, &apiEmails) +} + +func DeleteEmail(c *context.APIContext, form api.CreateEmailOption) { + if len(form.Emails) == 0 { + c.NoContent() + return + } + + emails := make([]*db.EmailAddress, len(form.Emails)) + for i := range form.Emails { + emails[i] = &db.EmailAddress{ + UID: c.User.ID, + Email: form.Emails[i], + } + } + + if err := db.DeleteEmailAddresses(emails); err != nil { + c.Error(http.StatusInternalServerError, "DeleteEmailAddresses", err) + return + } + c.NoContent() +} diff --git a/internal/route/api/v1/user/follower.go b/internal/route/api/v1/user/follower.go new file mode 100644 index 00000000..3a3d0298 --- /dev/null +++ b/internal/route/api/v1/user/follower.go @@ -0,0 +1,114 @@ +// Copyright 2015 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 user + +import ( + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" +) + +func responseApiUsers(c *context.APIContext, users []*db.User) { + apiUsers := make([]*api.User, len(users)) + for i := range users { + apiUsers[i] = users[i].APIFormat() + } + c.JSONSuccess(&apiUsers) +} + +func listUserFollowers(c *context.APIContext, u *db.User) { + users, err := u.GetFollowers(c.QueryInt("page")) + if err != nil { + c.ServerError("GetUserFollowers", err) + return + } + responseApiUsers(c, users) +} + +func ListMyFollowers(c *context.APIContext) { + listUserFollowers(c, c.User) +} + +func ListFollowers(c *context.APIContext) { + u := GetUserByParams(c) + if c.Written() { + return + } + listUserFollowers(c, u) +} + +func listUserFollowing(c *context.APIContext, u *db.User) { + users, err := u.GetFollowing(c.QueryInt("page")) + if err != nil { + c.ServerError("GetFollowing", err) + return + } + responseApiUsers(c, users) +} + +func ListMyFollowing(c *context.APIContext) { + listUserFollowing(c, c.User) +} + +func ListFollowing(c *context.APIContext) { + u := GetUserByParams(c) + if c.Written() { + return + } + listUserFollowing(c, u) +} + +func checkUserFollowing(c *context.APIContext, u *db.User, followID int64) { + if u.IsFollowing(followID) { + c.NoContent() + } else { + c.NotFound() + } +} + +func CheckMyFollowing(c *context.APIContext) { + target := GetUserByParams(c) + if c.Written() { + return + } + checkUserFollowing(c, c.User, target.ID) +} + +func CheckFollowing(c *context.APIContext) { + u := GetUserByParams(c) + if c.Written() { + return + } + target := GetUserByParamsName(c, ":target") + if c.Written() { + return + } + checkUserFollowing(c, u, target.ID) +} + +func Follow(c *context.APIContext) { + target := GetUserByParams(c) + if c.Written() { + return + } + if err := db.FollowUser(c.User.ID, target.ID); err != nil { + c.ServerError("FollowUser", err) + return + } + c.NoContent() +} + +func Unfollow(c *context.APIContext) { + target := GetUserByParams(c) + if c.Written() { + return + } + if err := db.UnfollowUser(c.User.ID, target.ID); err != nil { + c.ServerError("UnfollowUser", err) + return + } + c.NoContent() +} diff --git a/internal/route/api/v1/user/key.go b/internal/route/api/v1/user/key.go new file mode 100644 index 00000000..9cdb4e20 --- /dev/null +++ b/internal/route/api/v1/user/key.go @@ -0,0 +1,108 @@ +// Copyright 2015 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 user + +import ( + api "github.com/gogs/go-gogs-client" + convert2 "gogs.io/gogs/internal/route/api/v1/convert" + repo2 "gogs.io/gogs/internal/route/api/v1/repo" + "net/http" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/db/errors" + "gogs.io/gogs/internal/setting" +) + +func GetUserByParamsName(c *context.APIContext, name string) *db.User { + user, err := db.GetUserByName(c.Params(name)) + if err != nil { + c.NotFoundOrServerError("GetUserByName", errors.IsUserNotExist, err) + return nil + } + return user +} + +// GetUserByParams returns user whose name is presented in URL paramenter. +func GetUserByParams(c *context.APIContext) *db.User { + return GetUserByParamsName(c, ":username") +} + +func composePublicKeysAPILink() string { + return setting.AppURL + "api/v1/user/keys/" +} + +func listPublicKeys(c *context.APIContext, uid int64) { + keys, err := db.ListPublicKeys(uid) + if err != nil { + c.ServerError("ListPublicKeys", err) + return + } + + apiLink := composePublicKeysAPILink() + apiKeys := make([]*api.PublicKey, len(keys)) + for i := range keys { + apiKeys[i] = convert2.ToPublicKey(apiLink, keys[i]) + } + + c.JSONSuccess(&apiKeys) +} + +func ListMyPublicKeys(c *context.APIContext) { + listPublicKeys(c, c.User.ID) +} + +func ListPublicKeys(c *context.APIContext) { + user := GetUserByParams(c) + if c.Written() { + return + } + listPublicKeys(c, user.ID) +} + +func GetPublicKey(c *context.APIContext) { + key, err := db.GetPublicKeyByID(c.ParamsInt64(":id")) + if err != nil { + c.NotFoundOrServerError("GetPublicKeyByID", db.IsErrKeyNotExist, err) + return + } + + apiLink := composePublicKeysAPILink() + c.JSONSuccess(convert2.ToPublicKey(apiLink, key)) +} + +// CreateUserPublicKey creates new public key to given user by ID. +func CreateUserPublicKey(c *context.APIContext, form api.CreateKeyOption, uid int64) { + content, err := db.CheckPublicKeyString(form.Key) + if err != nil { + repo2.HandleCheckKeyStringError(c, err) + return + } + + key, err := db.AddPublicKey(uid, form.Title, content) + if err != nil { + repo2.HandleAddKeyError(c, err) + return + } + apiLink := composePublicKeysAPILink() + c.JSON(http.StatusCreated, convert2.ToPublicKey(apiLink, key)) +} + +func CreatePublicKey(c *context.APIContext, form api.CreateKeyOption) { + CreateUserPublicKey(c, form, c.User.ID) +} + +func DeletePublicKey(c *context.APIContext) { + if err := db.DeletePublicKey(c.User, c.ParamsInt64(":id")); err != nil { + if db.IsErrKeyAccessDenied(err) { + c.Error(http.StatusForbidden, "", "you do not have access to this key") + } else { + c.Error(http.StatusInternalServerError, "DeletePublicKey", err) + } + return + } + + c.NoContent() +} diff --git a/internal/route/api/v1/user/user.go b/internal/route/api/v1/user/user.go new file mode 100644 index 00000000..8da3b734 --- /dev/null +++ b/internal/route/api/v1/user/user.go @@ -0,0 +1,74 @@ +// Copyright 2014 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 user + +import ( + "net/http" + + "github.com/unknwon/com" + + api "github.com/gogs/go-gogs-client" + + "gogs.io/gogs/internal/context" + "gogs.io/gogs/internal/db" + "gogs.io/gogs/internal/db/errors" + "gogs.io/gogs/internal/markup" +) + +func Search(c *context.APIContext) { + opts := &db.SearchUserOptions{ + Keyword: c.Query("q"), + Type: db.USER_TYPE_INDIVIDUAL, + PageSize: com.StrTo(c.Query("limit")).MustInt(), + } + if opts.PageSize == 0 { + opts.PageSize = 10 + } + + users, _, err := db.SearchUserByName(opts) + if err != nil { + c.JSON(http.StatusInternalServerError, map[string]interface{}{ + "ok": false, + "error": err.Error(), + }) + return + } + + results := make([]*api.User, len(users)) + for i := range users { + results[i] = &api.User{ + ID: users[i].ID, + UserName: users[i].Name, + AvatarUrl: users[i].AvatarLink(), + FullName: markup.Sanitize(users[i].FullName), + } + if c.IsLogged { + results[i].Email = users[i].Email + } + } + + c.JSONSuccess(map[string]interface{}{ + "ok": true, + "data": results, + }) +} + +func GetInfo(c *context.APIContext) { + u, err := db.GetUserByName(c.Params(":username")) + if err != nil { + c.NotFoundOrServerError("GetUserByName", errors.IsUserNotExist, err) + return + } + + // Hide user e-mail when API caller isn't signed in. + if !c.IsLogged { + u.Email = "" + } + c.JSONSuccess(u.APIFormat()) +} + +func GetAuthenticatedUser(c *context.APIContext) { + c.JSONSuccess(c.User.APIFormat()) +} |