From ac4a10456ea4515091c3c90a83a82c1e59cdf428 Mon Sep 17 00:00:00 2001 From: Unknwon Date: Fri, 12 Dec 2014 20:30:32 -0500 Subject: [PATCH] api: able to create repo and fix #726 - POST /user/repos - POST /org/:org/repos --- cmd/web.go | 13 ++-- conf/locale/locale_de-DE.ini | 30 +++++++- gogs.go | 2 +- models/org.go | 22 +++++- modules/auth/repo_form.go | 2 +- modules/middleware/org.go | 2 +- modules/middleware/repo.go | 4 +- routers/api/v1/repo.go | 129 ++++++++++++++++++++++------------- routers/api/v1/repo_hooks.go | 15 +--- routers/api/v1/user.go | 10 +++ routers/repo/repo.go | 8 +-- routers/repo/setting.go | 2 +- templates/.VERSION | 2 +- templates/repo/create.tmpl | 2 +- 14 files changed, 162 insertions(+), 81 deletions(-) diff --git a/cmd/web.go b/cmd/web.go index 6f1319a4e..df9a380c1 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -26,6 +26,8 @@ import ( "github.com/macaron-contrib/session" "github.com/macaron-contrib/toolbox" + api "github.com/gogits/go-gogs-client" + "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/auth/apiv1" @@ -66,7 +68,7 @@ func checkVersion() { // Check dependency version. macaronVer := git.MustParseVersion(strings.Join(strings.Split(macaron.Version(), ".")[:3], ".")) - if macaronVer.LessThan(git.MustParseVersion("0.4.2")) { + if macaronVer.LessThan(git.MustParseVersion("0.4.7")) { log.Fatal(4, "Package macaron version is too old, did you forget to update?(github.com/Unknwon/macaron)") } i18nVer := git.MustParseVersion(i18n.Version()) @@ -200,14 +202,15 @@ func runWeb(*cli.Context) { }) // Repositories. - m.Get("/user/repos", middleware.ApiReqToken(), v1.ListMyRepos) + m.Combo("/user/repos", middleware.ApiReqToken()).Get(v1.ListMyRepos).Post(bind(api.CreateRepoOption{}), v1.CreateRepo) + m.Post("/org/:org/repos", middleware.ApiReqToken(), bind(api.CreateRepoOption{}), v1.CreateOrgRepo) m.Group("/repos", func() { m.Get("/search", v1.SearchRepos) - m.Post("/migrate", bindIgnErr(auth.MigrateRepoForm{}), v1.Migrate) + m.Post("/migrate", bindIgnErr(auth.MigrateRepoForm{}), v1.MigrateRepo) m.Group("/:username/:reponame", func() { - m.Combo("/hooks").Get(v1.ListRepoHooks).Post(bind(v1.CreateRepoHookForm{}), v1.CreateRepoHook) - m.Patch("/hooks/:id:int", bind(v1.EditRepoHookForm{}), v1.EditRepoHook) + m.Combo("/hooks").Get(v1.ListRepoHooks).Post(bind(api.CreateHookOption{}), v1.CreateRepoHook) + m.Patch("/hooks/:id:int", bind(api.EditHookOption{}), v1.EditRepoHook) m.Get("/raw/*", middleware.RepoRef(), v1.GetRepoRawFile) }, middleware.ApiRepoAssignment(), middleware.ApiReqToken()) }) diff --git a/conf/locale/locale_de-DE.ini b/conf/locale/locale_de-DE.ini index b40f2da5f..6a16aa215 100755 --- a/conf/locale/locale_de-DE.ini +++ b/conf/locale/locale_de-DE.ini @@ -208,7 +208,7 @@ enable_custom_avatar_helper=Aktiviere dies, um deinen Avatar nicht von Gravatar choose_new_avatar=Neuen Avatar auswählen update_avatar=Avatar-Einstellung aktualisieren uploaded_avatar_not_a_image=Die hochgeladene Datei ist kein Bild. -no_custom_avatar_available=No custom avatar available, cannot enable it. +no_custom_avatar_available=Kein benutzerdefinierter Avatar verfügbar, Aktivierung ist nicht möglich. update_avatar_success=Deine Avatar-Einstellung wurde aktualisiert. change_password=Passwort ändern @@ -377,6 +377,30 @@ diff.stats_desc= %d geänderte Dateien mit %d neuen Zei diff.bin=BIN diff.view_file=Datei anzeigen +release.releases=Releases +release.new_release=Neues Release +release.draft=Entwurf +release.prerelease=Pre-Release +release.stable=Endversion +release.edit=bearbeiten +release.ahead=%d Commits zu %s seit diesem Release +release.source_code=Quelltext +release.tag_name=Tag-Name +release.target=Ziel +release.tag_helper=Wähle ein neues Tag oder erstelle ein Tag beim Veröffentlichen. +release.release_title=Release-Titel +release.content_with_md=Inhalt mit Markdown +release.write=Schreiben +release.preview=Vorschau +release.content_placeholder=Schreibe hier etwas +release.loading=Laden… +release.prerelease_desc=Dies ist eine Pre-Release-Version +release.prerelease_helper=Wir möchten darauf hinweisen, dass dieses Release nicht für den produktiven Einsatz gedacht ist. +release.publish=Release veröffentlichen +release.save_draft=Entwurf speichern +release.edit_release=Release bearbeiten +release.tag_name_already_exist=Ein Release mit diesem Tag existiert bereits. + [org] org_name_holder=Name der Organisation org_name_helper=Gute Namen von Organisationen sind kurz und einprägsam. @@ -476,8 +500,8 @@ dashboard.delete_inactivate_accounts=inaktive Konten löschen dashboard.delete_inactivate_accounts_success=Alle inaktiven Konten wurden erfolgreich gelöscht. dashboard.delete_repo_archives=Alle Repository-Archive löschen dashboard.delete_repo_archives_success=Alle Repositoriy-Archive wurden gelöscht. -dashboard.git_gc_repos=Führe Garbage Collection auf Repositorys aus -dashboard.git_gc_repos_success=Garbage Collection wurde auf allen Repositorys erfolgreich ausgeführt. +dashboard.git_gc_repos=Führe Garbage Collection auf Repositories aus +dashboard.git_gc_repos_success=Garbage Collection wurde auf allen Repositories erfolgreich ausgeführt. dashboard.server_uptime=Server-Uptime dashboard.current_goroutine=Aktuelle Goroutines dashboard.current_memory_usage=Aktuelle Speichernutzung diff --git a/gogs.go b/gogs.go index de76ca722..88cc03a87 100644 --- a/gogs.go +++ b/gogs.go @@ -17,7 +17,7 @@ import ( "github.com/gogits/gogs/modules/setting" ) -const APP_VER = "0.5.8.1211 Beta" +const APP_VER = "0.5.8.1212 Beta" func init() { runtime.GOMAXPROCS(runtime.NumCPU()) diff --git a/models/org.go b/models/org.go index 41611f811..5431a111c 100644 --- a/models/org.go +++ b/models/org.go @@ -25,8 +25,8 @@ var ( ErrLastOrgOwner = errors.New("The user to remove is the last member in owner team") ) -// IsOrgOwner returns true if given user is in the owner team. -func (org *User) IsOrgOwner(uid int64) bool { +// IsOwnedBy returns true if given user is in the owner team. +func (org *User) IsOwnedBy(uid int64) bool { return IsOrganizationOwner(org.Id, uid) } @@ -170,6 +170,24 @@ func CreateOrganization(org, owner *User) (*User, error) { return org, sess.Commit() } +// GetOrgByName returns organization by given name. +func GetOrgByName(name string) (*User, error) { + if len(name) == 0 { + return nil, ErrOrgNotExist + } + u := &User{ + LowerName: strings.ToLower(name), + Type: ORGANIZATION, + } + has, err := x.Get(u) + if err != nil { + return nil, err + } else if !has { + return nil, ErrOrgNotExist + } + return u, nil +} + // CountOrganizations returns number of organizations. func CountOrganizations() int64 { count, _ := x.Where("type=1").Count(new(User)) diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index 41c0217a9..36e62f04f 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -21,9 +21,9 @@ type CreateRepoForm struct { RepoName string `form:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"` Private bool `form:"private"` Description string `form:"desc" binding:"MaxSize(255)"` + AutoInit bool `form:"auto_init"` Gitignore string `form:"gitignore"` License string `form:"license"` - InitReadme bool `form:"init_readme"` } func (f *CreateRepoForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { diff --git a/modules/middleware/org.go b/modules/middleware/org.go index be1029899..e68725861 100644 --- a/modules/middleware/org.go +++ b/modules/middleware/org.go @@ -48,7 +48,7 @@ func OrgAssignment(redirect bool, args ...bool) macaron.Handler { ctx.Data["Org"] = org if ctx.IsSigned { - ctx.Org.IsOwner = org.IsOrgOwner(ctx.User.Id) + ctx.Org.IsOwner = org.IsOwnedBy(ctx.User.Id) if ctx.Org.IsOwner { ctx.Org.IsMember = true ctx.Org.IsAdminTeam = true diff --git a/modules/middleware/repo.go b/modules/middleware/repo.go index 8b0f0c59f..171619492 100644 --- a/modules/middleware/repo.go +++ b/modules/middleware/repo.go @@ -55,7 +55,7 @@ func ApiRepoAssignment() macaron.Handler { ctx.Repo.Owner = u // Organization owner team members are true owners as well. - if ctx.IsSigned && ctx.Repo.Owner.IsOrganization() && ctx.Repo.Owner.IsOrgOwner(ctx.User.Id) { + if ctx.IsSigned && ctx.Repo.Owner.IsOrganization() && ctx.Repo.Owner.IsOwnedBy(ctx.User.Id) { ctx.Repo.IsTrueOwner = true } @@ -280,7 +280,7 @@ func RepoAssignment(redirect bool, args ...bool) macaron.Handler { ctx.Repo.Owner = u // Organization owner team members are true owners as well. - if ctx.IsSigned && ctx.Repo.Owner.IsOrganization() && ctx.Repo.Owner.IsOrgOwner(ctx.User.Id) { + if ctx.IsSigned && ctx.Repo.Owner.IsOrganization() && ctx.Repo.Owner.IsOwnedBy(ctx.User.Id) { ctx.Repo.IsTrueOwner = true } diff --git a/routers/api/v1/repo.go b/routers/api/v1/repo.go index 33e3b05a4..6f2372316 100644 --- a/routers/api/v1/repo.go +++ b/routers/api/v1/repo.go @@ -21,6 +21,26 @@ import ( "github.com/gogits/gogs/modules/setting" ) +// ToApiRepository converts repository to API format. +func ToApiRepository(owner *models.User, repo *models.Repository, permission api.Permission) *api.Repository { + sshUrlFmt := "%s@%s:%s/%s.git" + if setting.SshPort != 22 { + sshUrlFmt = "ssh://%s@%s:%d/%s/%s.git" + } + htmlUrl := setting.AppUrl + owner.Name + "/" + repo.Name + return &api.Repository{ + Id: repo.Id, + Owner: *ToApiUser(owner), + FullName: owner.Name + "/" + repo.Name, + Private: repo.IsPrivate, + Fork: repo.IsFork, + HtmlUrl: htmlUrl, + SshUrl: fmt.Sprintf(sshUrlFmt, setting.RunUser, setting.Domain, owner.LowerName, repo.LowerName), + CloneUrl: htmlUrl + ".git", + Permissions: permission, + } +} + func SearchRepos(ctx *middleware.Context) { opt := models.SearchOption{ Keyword: path.Base(ctx.Query("q")), @@ -44,7 +64,7 @@ func SearchRepos(ctx *middleware.Context) { }) return } - if u.IsOrganization() && u.IsOrgOwner(ctx.User.Id) { + if u.IsOrganization() && u.IsOwnedBy(ctx.User.Id) { opt.Private = true } // FIXME: how about collaborators? @@ -75,13 +95,66 @@ func SearchRepos(ctx *middleware.Context) { } } - ctx.Render.JSON(200, map[string]interface{}{ + ctx.JSON(200, map[string]interface{}{ "ok": true, "data": results, }) } -func Migrate(ctx *middleware.Context, form auth.MigrateRepoForm) { +func createRepo(ctx *middleware.Context, owner *models.User, opt api.CreateRepoOption) { + repo, err := models.CreateRepository(owner, opt.Name, opt.Description, + opt.Gitignore, opt.License, opt.Private, false, opt.AutoInit) + if err != nil { + if err == models.ErrRepoAlreadyExist || + err == models.ErrRepoNameIllegal { + ctx.JSON(422, &base.ApiJsonErr{err.Error(), base.DOC_URL}) + } else { + log.Error(4, "CreateRepository: %v", err) + if repo != nil { + if err = models.DeleteRepository(ctx.User.Id, repo.Id, ctx.User.Name); err != nil { + log.Error(4, "DeleteRepository: %v", err) + } + } + ctx.Error(500) + } + return + } + + ctx.JSON(200, ToApiRepository(owner, repo, api.Permission{true, true, true})) +} + +// POST /user/repos +// https://developer.github.com/v3/repos/#create +func CreateRepo(ctx *middleware.Context, opt api.CreateRepoOption) { + // Shouldn't reach this condition, but just in case. + if ctx.User.IsOrganization() { + ctx.JSON(422, "not allowed creating repository for organization") + return + } + createRepo(ctx, ctx.User, opt) +} + +// POST /orgs/:org/repos +// https://developer.github.com/v3/repos/#create +func CreateOrgRepo(ctx *middleware.Context, opt api.CreateRepoOption) { + org, err := models.GetOrgByName(ctx.Params(":org")) + if err != nil { + if err == models.ErrUserNotExist { + ctx.Error(404) + } else { + ctx.Error(500) + } + return + } + + if !org.IsOwnedBy(ctx.User.Id) { + ctx.Error(403) + return + } + createRepo(ctx, org, opt) +} + +func MigrateRepo(ctx *middleware.Context, form auth.MigrateRepoForm) { u, err := models.GetUserByName(ctx.Query("username")) if err != nil { ctx.JSON(500, map[string]interface{}{ @@ -103,17 +176,15 @@ func Migrate(ctx *middleware.Context, form auth.MigrateRepoForm) { if form.Uid != u.Id { org, err := models.GetUserById(form.Uid) if err != nil { - ctx.JSON(500, map[string]interface{}{ - "ok": false, - "error": err.Error(), - }) + log.Error(4, "GetUserById: %v", err) + ctx.Error(500) return } ctxUser = org } if ctx.HasError() { - ctx.JSON(500, map[string]interface{}{ + ctx.JSON(422, map[string]interface{}{ "ok": false, "error": ctx.GetErrMsg(), }) @@ -122,7 +193,7 @@ func Migrate(ctx *middleware.Context, form auth.MigrateRepoForm) { if ctxUser.IsOrganization() { // Check ownership of organization. - if !ctxUser.IsOrgOwner(u.Id) { + if !ctxUser.IsOwnedBy(u.Id) { ctx.JSON(403, map[string]interface{}{ "ok": false, "error": "given user is not owner of organization", @@ -173,29 +244,9 @@ func ListMyRepos(ctx *middleware.Context) { return } - sshUrlFmt := "%s@%s:%s/%s.git" - if setting.SshPort != 22 { - sshUrlFmt = "ssh://%s@%s:%d/%s/%s.git" - } - repos := make([]*api.Repository, numOwnRepos+len(collaRepos)) - // FIXME: make only one loop for i := range ownRepos { - repos[i] = &api.Repository{ - Id: ownRepos[i].Id, - Owner: api.User{ - Id: ctx.User.Id, - UserName: ctx.User.Name, - AvatarUrl: string(setting.Protocol) + ctx.User.AvatarLink(), - }, - FullName: ctx.User.Name + "/" + ownRepos[i].Name, - Private: ownRepos[i].IsPrivate, - Fork: ownRepos[i].IsFork, - HtmlUrl: setting.AppUrl + ctx.User.Name + "/" + ownRepos[i].Name, - SshUrl: fmt.Sprintf(sshUrlFmt, setting.RunUser, setting.Domain, ctx.User.LowerName, ownRepos[i].LowerName), - Permissions: api.Permission{true, true, true}, - } - repos[i].CloneUrl = repos[i].HtmlUrl + ".git" + repos[i] = ToApiRepository(ctx.User, ownRepos[i], api.Permission{true, true, true}) } for i := range collaRepos { if err = collaRepos[i].GetOwner(); err != nil { @@ -203,24 +254,10 @@ func ListMyRepos(ctx *middleware.Context) { return } j := i + numOwnRepos - repos[j] = &api.Repository{ - Id: collaRepos[i].Id, - Owner: api.User{ - Id: collaRepos[i].Owner.Id, - UserName: collaRepos[i].Owner.Name, - AvatarUrl: string(setting.Protocol) + collaRepos[i].Owner.AvatarLink(), - }, - FullName: collaRepos[i].Owner.Name + "/" + collaRepos[i].Name, - Private: collaRepos[i].IsPrivate, - Fork: collaRepos[i].IsFork, - HtmlUrl: setting.AppUrl + collaRepos[i].Owner.Name + "/" + collaRepos[i].Name, - SshUrl: fmt.Sprintf(sshUrlFmt, setting.RunUser, setting.Domain, collaRepos[i].Owner.LowerName, collaRepos[i].LowerName), - Permissions: api.Permission{false, collaRepos[i].CanPush, true}, - } - repos[j].CloneUrl = repos[j].HtmlUrl + ".git" + repos[j] = ToApiRepository(collaRepos[i].Owner, collaRepos[i].Repository, api.Permission{false, collaRepos[i].CanPush, true}) // FIXME: cache result to reduce DB query? - if collaRepos[i].Owner.IsOrganization() && collaRepos[i].Owner.IsOrgOwner(ctx.User.Id) { + if collaRepos[i].Owner.IsOrganization() && collaRepos[i].Owner.IsOwnedBy(ctx.User.Id) { repos[j].Permissions.Admin = true } } diff --git a/routers/api/v1/repo_hooks.go b/routers/api/v1/repo_hooks.go index 5dddbc5a3..afe18a005 100644 --- a/routers/api/v1/repo_hooks.go +++ b/routers/api/v1/repo_hooks.go @@ -48,15 +48,9 @@ func ListRepoHooks(ctx *middleware.Context) { ctx.JSON(200, &apiHooks) } -type CreateRepoHookForm struct { - Type string `json:"type" binding:"Required"` - Config map[string]string `json:"config" binding:"Required"` - Active bool `json:"active"` -} - // POST /repos/:username/:reponame/hooks // https://developer.github.com/v3/repos/hooks/#create-a-hook -func CreateRepoHook(ctx *middleware.Context, form CreateRepoHookForm) { +func CreateRepoHook(ctx *middleware.Context, form api.CreateHookOption) { if !models.IsValidHookTaskType(form.Type) { ctx.JSON(422, &base.ApiJsonErr{"invalid hook type", base.DOC_URL}) return @@ -124,14 +118,9 @@ func CreateRepoHook(ctx *middleware.Context, form CreateRepoHookForm) { ctx.JSON(201, apiHook) } -type EditRepoHookForm struct { - Config map[string]string `json:"config"` - Active *bool `json:"active"` -} - // PATCH /repos/:username/:reponame/hooks/:id // https://developer.github.com/v3/repos/hooks/#edit-a-hook -func EditRepoHook(ctx *middleware.Context, form EditRepoHookForm) { +func EditRepoHook(ctx *middleware.Context, form api.EditHookOption) { w, err := models.GetWebhookById(ctx.ParamsInt64(":id")) if err != nil { ctx.JSON(500, &base.ApiJsonErr{"GetWebhookById: " + err.Error(), base.DOC_URL}) diff --git a/routers/api/v1/user.go b/routers/api/v1/user.go index 15c423f72..e9ba615fc 100644 --- a/routers/api/v1/user.go +++ b/routers/api/v1/user.go @@ -12,8 +12,18 @@ import ( "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/middleware" + "github.com/gogits/gogs/modules/setting" ) +// ToApiUser converts user to API format. +func ToApiUser(u *models.User) *api.User { + return &api.User{ + Id: u.Id, + UserName: u.Name, + AvatarUrl: string(setting.Protocol) + u.AvatarLink(), + } +} + func SearchUsers(ctx *middleware.Context) { opt := models.SearchOption{ Keyword: ctx.Query("q"), diff --git a/routers/repo/repo.go b/routers/repo/repo.go index 8e4ace994..70b0c05ec 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -97,14 +97,14 @@ func CreatePost(ctx *middleware.Context, form auth.CreateRepoForm) { if ctxUser.IsOrganization() { // Check ownership of organization. - if !ctxUser.IsOrgOwner(ctx.User.Id) { + if !ctxUser.IsOwnedBy(ctx.User.Id) { ctx.Error(403) return } } repo, err := models.CreateRepository(ctxUser, form.RepoName, form.Description, - form.Gitignore, form.License, form.Private, false, form.InitReadme) + form.Gitignore, form.License, form.Private, false, form.AutoInit) if err == nil { log.Trace("Repository created: %s/%s", ctxUser.Name, repo.Name) ctx.Redirect(setting.AppSubUrl + "/" + ctxUser.Name + "/" + repo.Name) @@ -174,7 +174,7 @@ func MigratePost(ctx *middleware.Context, form auth.MigrateRepoForm) { if ctxUser.IsOrganization() { // Check ownership of organization. - if !ctxUser.IsOrgOwner(ctx.User.Id) { + if !ctxUser.IsOwnedBy(ctx.User.Id) { ctx.Error(403) return } @@ -292,7 +292,7 @@ func ForkPost(ctx *middleware.Context, form auth.CreateRepoForm) { if ctxUser.IsOrganization() { // Check ownership of organization. - if !ctxUser.IsOrgOwner(ctx.User.Id) { + if !ctxUser.IsOwnedBy(ctx.User.Id) { ctx.Error(403) return } diff --git a/routers/repo/setting.go b/routers/repo/setting.go index 94c642b2b..aec79aa43 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -137,7 +137,7 @@ func SettingsPost(ctx *middleware.Context, form auth.RepoSettingForm) { } if ctx.Repo.Owner.IsOrganization() { - if !ctx.Repo.Owner.IsOrgOwner(ctx.User.Id) { + if !ctx.Repo.Owner.IsOwnedBy(ctx.User.Id) { ctx.Error(404) return } diff --git a/templates/.VERSION b/templates/.VERSION index f69ca6c4b..e5ab0b331 100644 --- a/templates/.VERSION +++ b/templates/.VERSION @@ -1 +1 @@ -0.5.8.1211 Beta \ No newline at end of file +0.5.8.1212 Beta \ No newline at end of file diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index 5d0c9b0f1..8b60b5685 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -69,7 +69,7 @@
- + {{.i18n.Tr "repo.init_readme"}}