diff options
author | Unknwon <u@gogs.io> | 2017-06-11 00:34:14 -0400 |
---|---|---|
committer | Unknwon <u@gogs.io> | 2017-06-11 00:34:14 -0400 |
commit | 4400d2fdd933204044aeb18ce7d8613c53aa87c0 (patch) | |
tree | 841e91d5294c49b7335170fbc4b9ff79e882f91a /routes | |
parent | 6197a7639a88f7fb0fee8927e1d501504ae770ff (diff) |
Refactoring: rename package routers -> routes
Diffstat (limited to 'routes')
57 files changed, 12967 insertions, 0 deletions
diff --git a/routes/admin/admin.go b/routes/admin/admin.go new file mode 100644 index 00000000..0d5eb7a6 --- /dev/null +++ b/routes/admin/admin.go @@ -0,0 +1,258 @@ +// 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 admin + +import ( + "encoding/json" + "fmt" + "runtime" + "strings" + "time" + + "github.com/Unknwon/com" + "gopkg.in/macaron.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/cron" + "github.com/gogits/gogs/pkg/mailer" + "github.com/gogits/gogs/pkg/process" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/tool" +) + +const ( + DASHBOARD = "admin/dashboard" + CONFIG = "admin/config" + MONITOR = "admin/monitor" +) + +var ( + startTime = time.Now() +) + +var sysStatus struct { + Uptime string + NumGoroutine int + + // General statistics. + MemAllocated string // bytes allocated and still in use + MemTotal string // bytes allocated (even if freed) + MemSys string // bytes obtained from system (sum of XxxSys below) + Lookups uint64 // number of pointer lookups + MemMallocs uint64 // number of mallocs + MemFrees uint64 // number of frees + + // Main allocation heap statistics. + HeapAlloc string // bytes allocated and still in use + HeapSys string // bytes obtained from system + HeapIdle string // bytes in idle spans + HeapInuse string // bytes in non-idle span + HeapReleased string // bytes released to the OS + HeapObjects uint64 // total number of allocated objects + + // Low-level fixed-size structure allocator statistics. + // Inuse is bytes used now. + // Sys is bytes obtained from system. + StackInuse string // bootstrap stacks + StackSys string + MSpanInuse string // mspan structures + MSpanSys string + MCacheInuse string // mcache structures + MCacheSys string + BuckHashSys string // profiling bucket hash table + GCSys string // GC metadata + OtherSys string // other system allocations + + // Garbage collector statistics. + NextGC string // next run in HeapAlloc time (bytes) + LastGC string // last run in absolute time (ns) + PauseTotalNs string + PauseNs string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256] + NumGC uint32 +} + +func updateSystemStatus() { + sysStatus.Uptime = tool.TimeSincePro(startTime) + + m := new(runtime.MemStats) + runtime.ReadMemStats(m) + sysStatus.NumGoroutine = runtime.NumGoroutine() + + sysStatus.MemAllocated = tool.FileSize(int64(m.Alloc)) + sysStatus.MemTotal = tool.FileSize(int64(m.TotalAlloc)) + sysStatus.MemSys = tool.FileSize(int64(m.Sys)) + sysStatus.Lookups = m.Lookups + sysStatus.MemMallocs = m.Mallocs + sysStatus.MemFrees = m.Frees + + sysStatus.HeapAlloc = tool.FileSize(int64(m.HeapAlloc)) + sysStatus.HeapSys = tool.FileSize(int64(m.HeapSys)) + sysStatus.HeapIdle = tool.FileSize(int64(m.HeapIdle)) + sysStatus.HeapInuse = tool.FileSize(int64(m.HeapInuse)) + sysStatus.HeapReleased = tool.FileSize(int64(m.HeapReleased)) + sysStatus.HeapObjects = m.HeapObjects + + sysStatus.StackInuse = tool.FileSize(int64(m.StackInuse)) + sysStatus.StackSys = tool.FileSize(int64(m.StackSys)) + sysStatus.MSpanInuse = tool.FileSize(int64(m.MSpanInuse)) + sysStatus.MSpanSys = tool.FileSize(int64(m.MSpanSys)) + sysStatus.MCacheInuse = tool.FileSize(int64(m.MCacheInuse)) + sysStatus.MCacheSys = tool.FileSize(int64(m.MCacheSys)) + sysStatus.BuckHashSys = tool.FileSize(int64(m.BuckHashSys)) + sysStatus.GCSys = tool.FileSize(int64(m.GCSys)) + sysStatus.OtherSys = tool.FileSize(int64(m.OtherSys)) + + sysStatus.NextGC = tool.FileSize(int64(m.NextGC)) + sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000) + sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000) + sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000) + sysStatus.NumGC = m.NumGC +} + +// Operation types. +type AdminOperation int + +const ( + CLEAN_INACTIVATE_USER AdminOperation = iota + 1 + CLEAN_REPO_ARCHIVES + CLEAN_MISSING_REPOS + GIT_GC_REPOS + SYNC_SSH_AUTHORIZED_KEY + SYNC_REPOSITORY_HOOKS + REINIT_MISSING_REPOSITORY +) + +func Dashboard(c *context.Context) { + c.Data["Title"] = c.Tr("admin.dashboard") + c.Data["PageIsAdmin"] = true + c.Data["PageIsAdminDashboard"] = true + + // Run operation. + op, _ := com.StrTo(c.Query("op")).Int() + if op > 0 { + var err error + var success string + + switch AdminOperation(op) { + case CLEAN_INACTIVATE_USER: + success = c.Tr("admin.dashboard.delete_inactivate_accounts_success") + err = models.DeleteInactivateUsers() + case CLEAN_REPO_ARCHIVES: + success = c.Tr("admin.dashboard.delete_repo_archives_success") + err = models.DeleteRepositoryArchives() + case CLEAN_MISSING_REPOS: + success = c.Tr("admin.dashboard.delete_missing_repos_success") + err = models.DeleteMissingRepositories() + case GIT_GC_REPOS: + success = c.Tr("admin.dashboard.git_gc_repos_success") + err = models.GitGcRepos() + case SYNC_SSH_AUTHORIZED_KEY: + success = c.Tr("admin.dashboard.resync_all_sshkeys_success") + err = models.RewriteAllPublicKeys() + case SYNC_REPOSITORY_HOOKS: + success = c.Tr("admin.dashboard.resync_all_hooks_success") + err = models.SyncRepositoryHooks() + case REINIT_MISSING_REPOSITORY: + success = c.Tr("admin.dashboard.reinit_missing_repos_success") + err = models.ReinitMissingRepositories() + } + + if err != nil { + c.Flash.Error(err.Error()) + } else { + c.Flash.Success(success) + } + c.Redirect(setting.AppSubURL + "/admin") + return + } + + c.Data["Stats"] = models.GetStatistic() + // FIXME: update periodically + updateSystemStatus() + c.Data["SysStatus"] = sysStatus + c.HTML(200, DASHBOARD) +} + +func SendTestMail(c *context.Context) { + email := c.Query("email") + // Send a test email to the user's email address and redirect back to Config + if err := mailer.SendTestMail(email); err != nil { + c.Flash.Error(c.Tr("admin.config.test_mail_failed", email, err)) + } else { + c.Flash.Info(c.Tr("admin.config.test_mail_sent", email)) + } + + c.Redirect(setting.AppSubURL + "/admin/config") +} + +func Config(c *context.Context) { + c.Data["Title"] = c.Tr("admin.config") + c.Data["PageIsAdmin"] = true + c.Data["PageIsAdminConfig"] = true + + c.Data["AppURL"] = setting.AppURL + c.Data["Domain"] = setting.Domain + c.Data["OfflineMode"] = setting.OfflineMode + c.Data["DisableRouterLog"] = setting.DisableRouterLog + c.Data["RunUser"] = setting.RunUser + c.Data["RunMode"] = strings.Title(macaron.Env) + c.Data["StaticRootPath"] = setting.StaticRootPath + c.Data["LogRootPath"] = setting.LogRootPath + c.Data["ReverseProxyAuthUser"] = setting.ReverseProxyAuthUser + + c.Data["SSH"] = setting.SSH + + c.Data["RepoRootPath"] = setting.RepoRootPath + c.Data["ScriptType"] = setting.ScriptType + c.Data["Repository"] = setting.Repository + + c.Data["Service"] = setting.Service + c.Data["DbCfg"] = models.DbCfg + c.Data["Webhook"] = setting.Webhook + + c.Data["MailerEnabled"] = false + if setting.MailService != nil { + c.Data["MailerEnabled"] = true + c.Data["Mailer"] = setting.MailService + } + + c.Data["CacheAdapter"] = setting.CacheAdapter + c.Data["CacheInterval"] = setting.CacheInterval + c.Data["CacheConn"] = setting.CacheConn + + c.Data["SessionConfig"] = setting.SessionConfig + + c.Data["DisableGravatar"] = setting.DisableGravatar + c.Data["EnableFederatedAvatar"] = setting.EnableFederatedAvatar + + c.Data["GitVersion"] = setting.Git.Version + c.Data["Git"] = setting.Git + + type logger struct { + Mode, Config string + } + loggers := make([]*logger, len(setting.LogModes)) + for i := range setting.LogModes { + loggers[i] = &logger{ + Mode: strings.Title(setting.LogModes[i]), + } + + result, _ := json.MarshalIndent(setting.LogConfigs[i], "", " ") + loggers[i].Config = string(result) + } + c.Data["Loggers"] = loggers + + c.HTML(200, CONFIG) +} + +func Monitor(c *context.Context) { + c.Data["Title"] = c.Tr("admin.monitor") + c.Data["PageIsAdmin"] = true + c.Data["PageIsAdminMonitor"] = true + c.Data["Processes"] = process.Processes + c.Data["Entries"] = cron.ListTasks() + c.HTML(200, MONITOR) +} diff --git a/routes/admin/auths.go b/routes/admin/auths.go new file mode 100644 index 00000000..56a0aad6 --- /dev/null +++ b/routes/admin/auths.go @@ -0,0 +1,265 @@ +// 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 admin + +import ( + "fmt" + + "github.com/Unknwon/com" + "github.com/go-xorm/core" + log "gopkg.in/clog.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/auth/ldap" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/setting" +) + +const ( + AUTHS = "admin/auth/list" + AUTH_NEW = "admin/auth/new" + AUTH_EDIT = "admin/auth/edit" +) + +func Authentications(c *context.Context) { + c.Data["Title"] = c.Tr("admin.authentication") + c.Data["PageIsAdmin"] = true + c.Data["PageIsAdminAuthentications"] = true + + var err error + c.Data["Sources"], err = models.LoginSources() + if err != nil { + c.Handle(500, "LoginSources", err) + return + } + + c.Data["Total"] = models.CountLoginSources() + c.HTML(200, AUTHS) +} + +type dropdownItem struct { + Name string + Type interface{} +} + +var ( + authSources = []dropdownItem{ + {models.LoginNames[models.LOGIN_LDAP], models.LOGIN_LDAP}, + {models.LoginNames[models.LOGIN_DLDAP], models.LOGIN_DLDAP}, + {models.LoginNames[models.LOGIN_SMTP], models.LOGIN_SMTP}, + {models.LoginNames[models.LOGIN_PAM], models.LOGIN_PAM}, + } + securityProtocols = []dropdownItem{ + {models.SecurityProtocolNames[ldap.SECURITY_PROTOCOL_UNENCRYPTED], ldap.SECURITY_PROTOCOL_UNENCRYPTED}, + {models.SecurityProtocolNames[ldap.SECURITY_PROTOCOL_LDAPS], ldap.SECURITY_PROTOCOL_LDAPS}, + {models.SecurityProtocolNames[ldap.SECURITY_PROTOCOL_START_TLS], ldap.SECURITY_PROTOCOL_START_TLS}, + } +) + +func NewAuthSource(c *context.Context) { + c.Data["Title"] = c.Tr("admin.auths.new") + c.Data["PageIsAdmin"] = true + c.Data["PageIsAdminAuthentications"] = true + + c.Data["type"] = models.LOGIN_LDAP + c.Data["CurrentTypeName"] = models.LoginNames[models.LOGIN_LDAP] + c.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SECURITY_PROTOCOL_UNENCRYPTED] + c.Data["smtp_auth"] = "PLAIN" + c.Data["is_active"] = true + c.Data["AuthSources"] = authSources + c.Data["SecurityProtocols"] = securityProtocols + c.Data["SMTPAuths"] = models.SMTPAuths + c.HTML(200, AUTH_NEW) +} + +func parseLDAPConfig(f form.Authentication) *models.LDAPConfig { + return &models.LDAPConfig{ + Source: &ldap.Source{ + Name: f.Name, + Host: f.Host, + Port: f.Port, + SecurityProtocol: ldap.SecurityProtocol(f.SecurityProtocol), + SkipVerify: f.SkipVerify, + BindDN: f.BindDN, + UserDN: f.UserDN, + BindPassword: f.BindPassword, + UserBase: f.UserBase, + AttributeUsername: f.AttributeUsername, + AttributeName: f.AttributeName, + AttributeSurname: f.AttributeSurname, + AttributeMail: f.AttributeMail, + AttributesInBind: f.AttributesInBind, + Filter: f.Filter, + GroupEnabled: f.GroupEnabled, + GroupDN: f.GroupDN, + GroupFilter: f.GroupFilter, + GroupMemberUID: f.GroupMemberUID, + UserUID: f.UserUID, + AdminFilter: f.AdminFilter, + Enabled: true, + }, + } +} + +func parseSMTPConfig(f form.Authentication) *models.SMTPConfig { + return &models.SMTPConfig{ + Auth: f.SMTPAuth, + Host: f.SMTPHost, + Port: f.SMTPPort, + AllowedDomains: f.AllowedDomains, + TLS: f.TLS, + SkipVerify: f.SkipVerify, + } +} + +func NewAuthSourcePost(c *context.Context, f form.Authentication) { + c.Data["Title"] = c.Tr("admin.auths.new") + c.Data["PageIsAdmin"] = true + c.Data["PageIsAdminAuthentications"] = true + + c.Data["CurrentTypeName"] = models.LoginNames[models.LoginType(f.Type)] + c.Data["CurrentSecurityProtocol"] = models.SecurityProtocolNames[ldap.SecurityProtocol(f.SecurityProtocol)] + c.Data["AuthSources"] = authSources + c.Data["SecurityProtocols"] = securityProtocols + c.Data["SMTPAuths"] = models.SMTPAuths + + hasTLS := false + var config core.Conversion + switch models.LoginType(f.Type) { + case models.LOGIN_LDAP, models.LOGIN_DLDAP: + config = parseLDAPConfig(f) + hasTLS = ldap.SecurityProtocol(f.SecurityProtocol) > ldap.SECURITY_PROTOCOL_UNENCRYPTED + case models.LOGIN_SMTP: + config = parseSMTPConfig(f) + hasTLS = true + case models.LOGIN_PAM: + config = &models.PAMConfig{ + ServiceName: f.PAMServiceName, + } + default: + c.Error(400) + return + } + c.Data["HasTLS"] = hasTLS + + if c.HasError() { + c.HTML(200, AUTH_NEW) + return + } + + if err := models.CreateLoginSource(&models.LoginSource{ + Type: models.LoginType(f.Type), + Name: f.Name, + IsActived: f.IsActive, + Cfg: config, + }); err != nil { + if models.IsErrLoginSourceAlreadyExist(err) { + c.Data["Err_Name"] = true + c.RenderWithErr(c.Tr("admin.auths.login_source_exist", err.(models.ErrLoginSourceAlreadyExist).Name), AUTH_NEW, f) + } else { + c.Handle(500, "CreateSource", err) + } + return + } + + log.Trace("Authentication created by admin(%s): %s", c.User.Name, f.Name) + + c.Flash.Success(c.Tr("admin.auths.new_success", f.Name)) + c.Redirect(setting.AppSubURL + "/admin/auths") +} + +func EditAuthSource(c *context.Context) { + c.Data["Title"] = c.Tr("admin.auths.edit") + c.Data["PageIsAdmin"] = true + c.Data["PageIsAdminAuthentications"] = true + + c.Data["SecurityProtocols"] = securityProtocols + c.Data["SMTPAuths"] = models.SMTPAuths + + source, err := models.GetLoginSourceByID(c.ParamsInt64(":authid")) + if err != nil { + c.Handle(500, "GetLoginSourceByID", err) + return + } + c.Data["Source"] = source + c.Data["HasTLS"] = source.HasTLS() + + c.HTML(200, AUTH_EDIT) +} + +func EditAuthSourcePost(c *context.Context, f form.Authentication) { + c.Data["Title"] = c.Tr("admin.auths.edit") + c.Data["PageIsAdmin"] = true + c.Data["PageIsAdminAuthentications"] = true + + c.Data["SMTPAuths"] = models.SMTPAuths + + source, err := models.GetLoginSourceByID(c.ParamsInt64(":authid")) + if err != nil { + c.Handle(500, "GetLoginSourceByID", err) + return + } + c.Data["Source"] = source + c.Data["HasTLS"] = source.HasTLS() + + if c.HasError() { + c.HTML(200, AUTH_EDIT) + return + } + + var config core.Conversion + switch models.LoginType(f.Type) { + case models.LOGIN_LDAP, models.LOGIN_DLDAP: + config = parseLDAPConfig(f) + case models.LOGIN_SMTP: + config = parseSMTPConfig(f) + case models.LOGIN_PAM: + config = &models.PAMConfig{ + ServiceName: f.PAMServiceName, + } + default: + c.Error(400) + return + } + + source.Name = f.Name + source.IsActived = f.IsActive + source.Cfg = config + if err := models.UpdateSource(source); err != nil { + c.Handle(500, "UpdateSource", err) + return + } + log.Trace("Authentication changed by admin(%s): %d", c.User.Name, source.ID) + + c.Flash.Success(c.Tr("admin.auths.update_success")) + c.Redirect(setting.AppSubURL + "/admin/auths/" + com.ToStr(f.ID)) +} + +func DeleteAuthSource(c *context.Context) { + source, err := models.GetLoginSourceByID(c.ParamsInt64(":authid")) + if err != nil { + c.Handle(500, "GetLoginSourceByID", err) + return + } + + if err = models.DeleteSource(source); err != nil { + if models.IsErrLoginSourceInUse(err) { + c.Flash.Error(c.Tr("admin.auths.still_in_used")) + } else { + c.Flash.Error(fmt.Sprintf("DeleteSource: %v", err)) + } + c.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/admin/auths/" + c.Params(":authid"), + }) + return + } + log.Trace("Authentication deleted by admin(%s): %d", c.User.Name, source.ID) + + c.Flash.Success(c.Tr("admin.auths.deletion_success")) + c.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/admin/auths", + }) +} diff --git a/routes/admin/notice.go b/routes/admin/notice.go new file mode 100644 index 00000000..c743a1da --- /dev/null +++ b/routes/admin/notice.go @@ -0,0 +1,72 @@ +// 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 admin + +import ( + "github.com/Unknwon/com" + "github.com/Unknwon/paginater" + log "gopkg.in/clog.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" +) + +const ( + NOTICES = "admin/notice" +) + +func Notices(c *context.Context) { + c.Data["Title"] = c.Tr("admin.notices") + c.Data["PageIsAdmin"] = true + c.Data["PageIsAdminNotices"] = true + + total := models.CountNotices() + page := c.QueryInt("page") + if page <= 1 { + page = 1 + } + c.Data["Page"] = paginater.New(int(total), setting.UI.Admin.NoticePagingNum, page, 5) + + notices, err := models.Notices(page, setting.UI.Admin.NoticePagingNum) + if err != nil { + c.Handle(500, "Notices", err) + return + } + c.Data["Notices"] = notices + + c.Data["Total"] = total + c.HTML(200, NOTICES) +} + +func DeleteNotices(c *context.Context) { + strs := c.QueryStrings("ids[]") + ids := make([]int64, 0, len(strs)) + for i := range strs { + id := com.StrTo(strs[i]).MustInt64() + if id > 0 { + ids = append(ids, id) + } + } + + if err := models.DeleteNoticesByIDs(ids); err != nil { + c.Flash.Error("DeleteNoticesByIDs: " + err.Error()) + c.Status(500) + } else { + c.Flash.Success(c.Tr("admin.notices.delete_success")) + c.Status(200) + } +} + +func EmptyNotices(c *context.Context) { + if err := models.DeleteNotices(0, 0); err != nil { + c.Handle(500, "DeleteNotices", err) + return + } + + log.Trace("System notices deleted by admin (%s): [start: %d]", c.User.Name, 0) + c.Flash.Success(c.Tr("admin.notices.delete_success")) + c.Redirect(setting.AppSubURL + "/admin/notices") +} diff --git a/routes/admin/orgs.go b/routes/admin/orgs.go new file mode 100644 index 00000000..f42e1fdf --- /dev/null +++ b/routes/admin/orgs.go @@ -0,0 +1,31 @@ +// 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 admin + +import ( + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/routes" +) + +const ( + ORGS = "admin/org/list" +) + +func Organizations(c *context.Context) { + c.Data["Title"] = c.Tr("admin.organizations") + c.Data["PageIsAdmin"] = true + c.Data["PageIsAdminOrganizations"] = true + + routes.RenderUserSearch(c, &routes.UserSearchOptions{ + Type: models.USER_TYPE_ORGANIZATION, + Counter: models.CountOrganizations, + Ranger: models.Organizations, + PageSize: setting.UI.Admin.OrgPagingNum, + OrderBy: "id ASC", + TplName: ORGS, + }) +} diff --git a/routes/admin/repos.go b/routes/admin/repos.go new file mode 100644 index 00000000..b4fa2266 --- /dev/null +++ b/routes/admin/repos.go @@ -0,0 +1,87 @@ +// 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 admin + +import ( + "github.com/Unknwon/paginater" + log "gopkg.in/clog.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" +) + +const ( + REPOS = "admin/repo/list" +) + +func Repos(c *context.Context) { + c.Data["Title"] = c.Tr("admin.repositories") + c.Data["PageIsAdmin"] = true + c.Data["PageIsAdminRepositories"] = true + + page := c.QueryInt("page") + if page <= 0 { + page = 1 + } + + var ( + repos []*models.Repository + count int64 + err error + ) + + keyword := c.Query("q") + if len(keyword) == 0 { + repos, err = models.Repositories(page, setting.UI.Admin.RepoPagingNum) + if err != nil { + c.Handle(500, "Repositories", err) + return + } + count = models.CountRepositories(true) + } else { + repos, count, err = models.SearchRepositoryByName(&models.SearchRepoOptions{ + Keyword: keyword, + OrderBy: "id ASC", + Private: true, + Page: page, + PageSize: setting.UI.Admin.RepoPagingNum, + }) + if err != nil { + c.Handle(500, "SearchRepositoryByName", err) + return + } + } + c.Data["Keyword"] = keyword + c.Data["Total"] = count + c.Data["Page"] = paginater.New(int(count), setting.UI.Admin.RepoPagingNum, page, 5) + + if err = models.RepositoryList(repos).LoadAttributes(); err != nil { + c.Handle(500, "LoadAttributes", err) + return + } + c.Data["Repos"] = repos + + c.HTML(200, REPOS) +} + +func DeleteRepo(c *context.Context) { + repo, err := models.GetRepositoryByID(c.QueryInt64("id")) + if err != nil { + c.Handle(500, "GetRepositoryByID", err) + return + } + + if err := models.DeleteRepository(repo.MustOwner().ID, repo.ID); err != nil { + c.Handle(500, "DeleteRepository", err) + return + } + log.Trace("Repository deleted: %s/%s", repo.MustOwner().Name, repo.Name) + + c.Flash.Success(c.Tr("repo.settings.deletion_success")) + c.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/admin/repos?page=" + c.Query("page"), + }) +} diff --git a/routes/admin/users.go b/routes/admin/users.go new file mode 100644 index 00000000..cfeb73de --- /dev/null +++ b/routes/admin/users.go @@ -0,0 +1,262 @@ +// 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 admin + +import ( + "strings" + + "github.com/Unknwon/com" + log "gopkg.in/clog.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/mailer" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/routes" +) + +const ( + USERS = "admin/user/list" + USER_NEW = "admin/user/new" + USER_EDIT = "admin/user/edit" +) + +func Users(c *context.Context) { + c.Data["Title"] = c.Tr("admin.users") + c.Data["PageIsAdmin"] = true + c.Data["PageIsAdminUsers"] = true + + routes.RenderUserSearch(c, &routes.UserSearchOptions{ + Type: models.USER_TYPE_INDIVIDUAL, + Counter: models.CountUsers, + Ranger: models.Users, + PageSize: setting.UI.Admin.UserPagingNum, + OrderBy: "id ASC", + TplName: USERS, + }) +} + +func NewUser(c *context.Context) { + c.Data["Title"] = c.Tr("admin.users.new_account") + c.Data["PageIsAdmin"] = true + c.Data["PageIsAdminUsers"] = true + + c.Data["login_type"] = "0-0" + + sources, err := models.LoginSources() + if err != nil { + c.Handle(500, "LoginSources", err) + return + } + c.Data["Sources"] = sources + + c.Data["CanSendEmail"] = setting.MailService != nil + c.HTML(200, USER_NEW) +} + +func NewUserPost(c *context.Context, f form.AdminCrateUser) { + c.Data["Title"] = c.Tr("admin.users.new_account") + c.Data["PageIsAdmin"] = true + c.Data["PageIsAdminUsers"] = true + + sources, err := models.LoginSources() + if err != nil { + c.Handle(500, "LoginSources", err) + return + } + c.Data["Sources"] = sources + + c.Data["CanSendEmail"] = setting.MailService != nil + + if c.HasError() { + c.HTML(200, USER_NEW) + return + } + + u := &models.User{ + Name: f.UserName, + Email: f.Email, + Passwd: f.Password, + IsActive: true, + LoginType: models.LOGIN_PLAIN, + } + + if len(f.LoginType) > 0 { + fields := strings.Split(f.LoginType, "-") + if len(fields) == 2 { + u.LoginType = models.LoginType(com.StrTo(fields[0]).MustInt()) + u.LoginSource = com.StrTo(fields[1]).MustInt64() + u.LoginName = f.LoginName + } + } + + if err := models.CreateUser(u); err != nil { + switch { + case models.IsErrUserAlreadyExist(err): + c.Data["Err_UserName"] = true + c.RenderWithErr(c.Tr("form.username_been_taken"), USER_NEW, &f) + case models.IsErrEmailAlreadyUsed(err): + c.Data["Err_Email"] = true + c.RenderWithErr(c.Tr("form.email_been_used"), USER_NEW, &f) + case models.IsErrNameReserved(err): + c.Data["Err_UserName"] = true + c.RenderWithErr(c.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), USER_NEW, &f) + case models.IsErrNamePatternNotAllowed(err): + c.Data["Err_UserName"] = true + c.RenderWithErr(c.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), USER_NEW, &f) + default: + c.Handle(500, "CreateUser", err) + } + return + } + log.Trace("Account created by admin (%s): %s", c.User.Name, u.Name) + + // Send email notification. + if f.SendNotify && setting.MailService != nil { + mailer.SendRegisterNotifyMail(c.Context, models.NewMailerUser(u)) + } + + c.Flash.Success(c.Tr("admin.users.new_success", u.Name)) + c.Redirect(setting.AppSubURL + "/admin/users/" + com.ToStr(u.ID)) +} + +func prepareUserInfo(c *context.Context) *models.User { + u, err := models.GetUserByID(c.ParamsInt64(":userid")) + if err != nil { + c.Handle(500, "GetUserByID", err) + return nil + } + c.Data["User"] = u + + if u.LoginSource > 0 { + c.Data["LoginSource"], err = models.GetLoginSourceByID(u.LoginSource) + if err != nil { + c.Handle(500, "GetLoginSourceByID", err) + return nil + } + } else { + c.Data["LoginSource"] = &models.LoginSource{} + } + + sources, err := models.LoginSources() + if err != nil { + c.Handle(500, "LoginSources", err) + return nil + } + c.Data["Sources"] = sources + + return u +} + +func EditUser(c *context.Context) { + c.Data["Title"] = c.Tr("admin.users.edit_account") + c.Data["PageIsAdmin"] = true + c.Data["PageIsAdminUsers"] = true + c.Data["EnableLocalPathMigration"] = setting.Repository.EnableLocalPathMigration + + prepareUserInfo(c) + if c.Written() { + return + } + + c.HTML(200, USER_EDIT) +} + +func EditUserPost(c *context.Context, f form.AdminEditUser) { + c.Data["Title"] = c.Tr("admin.users.edit_account") + c.Data["PageIsAdmin"] = true + c.Data["PageIsAdminUsers"] = true + c.Data["EnableLocalPathMigration"] = setting.Repository.EnableLocalPathMigration + + u := prepareUserInfo(c) + if c.Written() { + return + } + + if c.HasError() { + c.HTML(200, USER_EDIT) + return + } + + fields := strings.Split(f.LoginType, "-") + if len(fields) == 2 { + loginType := models.LoginType(com.StrTo(fields[0]).MustInt()) + loginSource := com.StrTo(fields[1]).MustInt64() + + if u.LoginSource != loginSource { + u.LoginSource = loginSource + u.LoginType = loginType + } + } + + if len(f.Password) > 0 { + u.Passwd = f.Password + var err error + if u.Salt, err = models.GetUserSalt(); err != nil { + c.Handle(500, "UpdateUser", err) + return + } + u.EncodePasswd() + } + + u.LoginName = f.LoginName + u.FullName = f.FullName + u.Email = f.Email + u.Website = f.Website + u.Location = f.Location + u.MaxRepoCreation = f.MaxRepoCreation + u.IsActive = f.Active + u.IsAdmin = f.Admin + u.AllowGitHook = f.AllowGitHook + u.AllowImportLocal = f.AllowImportLocal + u.ProhibitLogin = f.ProhibitLogin + + if err := models.UpdateUser(u); err != nil { + if models.IsErrEmailAlreadyUsed(err) { + c.Data["Err_Email"] = true + c.RenderWithErr(c.Tr("form.email_been_used"), USER_EDIT, &f) + } else { + c.Handle(500, "UpdateUser", err) + } + return + } + log.Trace("Account profile updated by admin (%s): %s", c.User.Name, u.Name) + + c.Flash.Success(c.Tr("admin.users.update_profile_success")) + c.Redirect(setting.AppSubURL + "/admin/users/" + c.Params(":userid")) +} + +func DeleteUser(c *context.Context) { + u, err := models.GetUserByID(c.ParamsInt64(":userid")) + if err != nil { + c.Handle(500, "GetUserByID", err) + return + } + + if err = models.DeleteUser(u); err != nil { + switch { + case models.IsErrUserOwnRepos(err): + c.Flash.Error(c.Tr("admin.users.still_own_repo")) + c.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/admin/users/" + c.Params(":userid"), + }) + case models.IsErrUserHasOrgs(err): + c.Flash.Error(c.Tr("admin.users.still_has_org")) + c.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/admin/users/" + c.Params(":userid"), + }) + default: + c.Handle(500, "DeleteUser", err) + } + return + } + log.Trace("Account deleted by admin (%s): %s", c.User.Name, u.Name) + + c.Flash.Success(c.Tr("admin.users.deletion_success")) + c.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/admin/users", + }) +} diff --git a/routes/api/v1/admin/org.go b/routes/api/v1/admin/org.go new file mode 100644 index 00000000..0f84ed2e --- /dev/null +++ b/routes/api/v1/admin/org.go @@ -0,0 +1,44 @@ +// 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/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/routes/api/v1/convert" + "github.com/gogits/gogs/routes/api/v1/user" +) + +// https://github.com/gogits/go-gogs-client/wiki/Administration-Organizations#create-a-new-organization +func CreateOrg(c *context.APIContext, form api.CreateOrgOption) { + u := user.GetUserByParams(c) + if c.Written() { + return + } + + org := &models.User{ + Name: form.UserName, + FullName: form.FullName, + Description: form.Description, + Website: form.Website, + Location: form.Location, + IsActive: true, + Type: models.USER_TYPE_ORGANIZATION, + } + if err := models.CreateOrganization(org, u); err != nil { + if models.IsErrUserAlreadyExist(err) || + models.IsErrNameReserved(err) || + models.IsErrNamePatternNotAllowed(err) { + c.Error(422, "", err) + } else { + c.Error(500, "CreateOrganization", err) + } + return + } + + c.JSON(201, convert.ToOrganization(org)) +} diff --git a/routes/api/v1/admin/org_repo.go b/routes/api/v1/admin/org_repo.go new file mode 100644 index 00000000..7abad1a8 --- /dev/null +++ b/routes/api/v1/admin/org_repo.go @@ -0,0 +1,50 @@ +// 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 ( + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" +) + +func GetRepositoryByParams(c *context.APIContext) *models.Repository { + repo, err := models.GetRepositoryByName(c.Org.Team.OrgID, c.Params(":reponame")) + if err != nil { + if errors.IsRepoNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetRepositoryByName", 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.Error(500, "AddRepository", err) + return + } + + c.Status(204) +} + +func RemoveTeamRepository(c *context.APIContext) { + repo := GetRepositoryByParams(c) + if c.Written() { + return + } + if err := c.Org.Team.RemoveRepository(repo.ID); err != nil { + c.Error(500, "RemoveRepository", err) + return + } + + c.Status(204) +} diff --git a/routes/api/v1/admin/org_team.go b/routes/api/v1/admin/org_team.go new file mode 100644 index 00000000..ae748504 --- /dev/null +++ b/routes/api/v1/admin/org_team.go @@ -0,0 +1,60 @@ +// 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 ( + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/routes/api/v1/convert" + "github.com/gogits/gogs/routes/api/v1/user" +) + +func CreateTeam(c *context.APIContext, form api.CreateTeamOption) { + team := &models.Team{ + OrgID: c.Org.Organization.ID, + Name: form.Name, + Description: form.Description, + Authorize: models.ParseAccessMode(form.Permission), + } + if err := models.NewTeam(team); err != nil { + if models.IsErrTeamAlreadyExist(err) { + c.Error(422, "", err) + } else { + c.Error(500, "NewTeam", err) + } + return + } + + c.JSON(201, convert.ToTeam(team)) +} + +func AddTeamMember(c *context.APIContext) { + u := user.GetUserByParams(c) + if c.Written() { + return + } + if err := c.Org.Team.AddMember(u.ID); err != nil { + c.Error(500, "AddMember", err) + return + } + + c.Status(204) +} + +func RemoveTeamMember(c *context.APIContext) { + u := user.GetUserByParams(c) + if c.Written() { + return + } + + if err := c.Org.Team.RemoveMember(u.ID); err != nil { + c.Error(500, "RemoveMember", err) + return + } + + c.Status(204) +} diff --git a/routes/api/v1/admin/repo.go b/routes/api/v1/admin/repo.go new file mode 100644 index 00000000..920bac8d --- /dev/null +++ b/routes/api/v1/admin/repo.go @@ -0,0 +1,23 @@ +// 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/gogits/go-gogs-client" + + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/routes/api/v1/repo" + "github.com/gogits/gogs/routes/api/v1/user" +) + +// https://github.com/gogits/go-gogs-client/wiki/Administration-Repositories#create-a-new-repository +func CreateRepo(c *context.APIContext, form api.CreateRepoOption) { + owner := user.GetUserByParams(c) + if c.Written() { + return + } + + repo.CreateUserRepo(c, owner, form) +} diff --git a/routes/api/v1/admin/user.go b/routes/api/v1/admin/user.go new file mode 100644 index 00000000..623911fd --- /dev/null +++ b/routes/api/v1/admin/user.go @@ -0,0 +1,160 @@ +// 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 ( + log "gopkg.in/clog.v1" + + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/mailer" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/routes/api/v1/user" +) + +func parseLoginSource(c *context.APIContext, u *models.User, sourceID int64, loginName string) { + if sourceID == 0 { + return + } + + source, err := models.GetLoginSourceByID(sourceID) + if err != nil { + if models.IsErrLoginSourceNotExist(err) { + c.Error(422, "", err) + } else { + c.Error(500, "GetLoginSourceByID", err) + } + return + } + + u.LoginType = source.Type + u.LoginSource = source.ID + u.LoginName = loginName +} + +// https://github.com/gogits/go-gogs-client/wiki/Administration-Users#create-a-new-user +func CreateUser(c *context.APIContext, form api.CreateUserOption) { + u := &models.User{ + Name: form.Username, + FullName: form.FullName, + Email: form.Email, + Passwd: form.Password, + IsActive: true, + LoginType: models.LOGIN_PLAIN, + } + + parseLoginSource(c, u, form.SourceID, form.LoginName) + if c.Written() { + return + } + + if err := models.CreateUser(u); err != nil { + if models.IsErrUserAlreadyExist(err) || + models.IsErrEmailAlreadyUsed(err) || + models.IsErrNameReserved(err) || + models.IsErrNamePatternNotAllowed(err) { + c.Error(422, "", err) + } else { + c.Error(500, "CreateUser", err) + } + return + } + log.Trace("Account created by admin (%s): %s", c.User.Name, u.Name) + + // Send email notification. + if form.SendNotify && setting.MailService != nil { + mailer.SendRegisterNotifyMail(c.Context.Context, models.NewMailerUser(u)) + } + + c.JSON(201, u.APIFormat()) +} + +// https://github.com/gogits/go-gogs-client/wiki/Administration-Users#edit-an-existing-user +func EditUser(c *context.APIContext, form api.EditUserOption) { + u := user.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 = models.GetUserSalt(); err != nil { + c.Error(500, "UpdateUser", 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 := models.UpdateUser(u); err != nil { + if models.IsErrEmailAlreadyUsed(err) { + c.Error(422, "", err) + } else { + c.Error(500, "UpdateUser", err) + } + return + } + log.Trace("Account profile updated by admin (%s): %s", c.User.Name, u.Name) + + c.JSON(200, u.APIFormat()) +} + +// https://github.com/gogits/go-gogs-client/wiki/Administration-Users#delete-a-user +func DeleteUser(c *context.APIContext) { + u := user.GetUserByParams(c) + if c.Written() { + return + } + + if err := models.DeleteUser(u); err != nil { + if models.IsErrUserOwnRepos(err) || + models.IsErrUserHasOrgs(err) { + c.Error(422, "", err) + } else { + c.Error(500, "DeleteUser", err) + } + return + } + log.Trace("Account deleted by admin(%s): %s", c.User.Name, u.Name) + + c.Status(204) +} + +// https://github.com/gogits/go-gogs-client/wiki/Administration-Users#create-a-public-key-for-user +func CreatePublicKey(c *context.APIContext, form api.CreateKeyOption) { + u := user.GetUserByParams(c) + if c.Written() { + return + } + user.CreateUserPublicKey(c, form, u.ID) +} diff --git a/routes/api/v1/api.go b/routes/api/v1/api.go new file mode 100644 index 00000000..510c54cf --- /dev/null +++ b/routes/api/v1/api.go @@ -0,0 +1,356 @@ +// 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 ( + "strings" + + "github.com/go-macaron/binding" + "gopkg.in/macaron.v1" + + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/routes/api/v1/admin" + "github.com/gogits/gogs/routes/api/v1/misc" + "github.com/gogits/gogs/routes/api/v1/org" + "github.com/gogits/gogs/routes/api/v1/repo" + "github.com/gogits/gogs/routes/api/v1/user" +) + +func repoAssignment() macaron.Handler { + return func(c *context.APIContext) { + userName := c.Params(":username") + repoName := c.Params(":reponame") + + var ( + owner *models.User + err error + ) + + // Check if the user is the same as the repository owner. + if c.IsLogged && c.User.LowerName == strings.ToLower(userName) { + owner = c.User + } else { + owner, err = models.GetUserByName(userName) + if err != nil { + if errors.IsUserNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetUserByName", err) + } + return + } + } + c.Repo.Owner = owner + + // Get repository. + repo, err := models.GetRepositoryByName(owner.ID, repoName) + if err != nil { + if errors.IsRepoNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetRepositoryByName", err) + } + return + } else if err = repo.GetOwner(); err != nil { + c.Error(500, "GetOwner", err) + return + } + + if c.IsLogged && c.User.IsAdmin { + c.Repo.AccessMode = models.ACCESS_MODE_OWNER + } else { + mode, err := models.AccessLevel(c.User.ID, repo) + if err != nil { + c.Error(500, "AccessLevel", err) + return + } + c.Repo.AccessMode = mode + } + + if !c.Repo.HasAccess() { + c.Status(404) + return + } + + c.Repo.Repository = repo + } +} + +// Contexter middleware already checks token for user sign in process. +func reqToken() macaron.Handler { + return func(c *context.Context) { + if !c.IsLogged { + c.Error(401) + return + } + } +} + +func reqBasicAuth() macaron.Handler { + return func(c *context.Context) { + if !c.IsBasicAuth { + c.Error(401) + return + } + } +} + +func reqAdmin() macaron.Handler { + return func(c *context.Context) { + if !c.IsLogged || !c.User.IsAdmin { + c.Error(403) + return + } + } +} + +func reqRepoWriter() macaron.Handler { + return func(c *context.Context) { + if !c.Repo.IsWriter() { + c.Error(403) + return + } + } +} + +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 = models.GetUserByName(c.Params(":orgname")) + if err != nil { + if errors.IsUserNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetUserByName", err) + } + return + } + } + + if assignTeam { + c.Org.Team, err = models.GetTeamByID(c.ParamsInt64(":teamid")) + if err != nil { + if errors.IsUserNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetTeamById", err) + } + return + } + } + } +} + +func mustEnableIssues(c *context.APIContext) { + if !c.Repo.Repository.EnableIssues || c.Repo.Repository.EnableExternalTracker { + c.Status(404) + return + } +} + +// RegisterRoutes registers all v1 APIs routes to 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{}), misc.Markdown) + m.Post("/markdown/raw", misc.MarkdownRaw) + + // Users + m.Group("/users", func() { + m.Get("/search", user.Search) + + m.Group("/:username", func() { + m.Get("", user.GetInfo) + + m.Group("/tokens", func() { + m.Combo("").Get(user.ListAccessTokens). + Post(bind(api.CreateAccessTokenOption{}), user.CreateAccessToken) + }, reqBasicAuth()) + }) + }) + + m.Group("/users", func() { + m.Group("/:username", func() { + m.Get("/keys", user.ListPublicKeys) + + m.Get("/followers", user.ListFollowers) + m.Group("/following", func() { + m.Get("", user.ListFollowing) + m.Get("/:target", user.CheckFollowing) + }) + }) + }, reqToken()) + + m.Group("/user", func() { + m.Get("", user.GetAuthenticatedUser) + m.Combo("/emails").Get(user.ListEmails). + Post(bind(api.CreateEmailOption{}), user.AddEmail). + Delete(bind(api.CreateEmailOption{}), user.DeleteEmail) + + m.Get("/followers", user.ListMyFollowers) + m.Group("/following", func() { + m.Get("", user.ListMyFollowing) + m.Combo("/:username").Get(user.CheckMyFollowing).Put(user.Follow).Delete(user.Unfollow) + }) + + m.Group("/keys", func() { + m.Combo("").Get(user.ListMyPublicKeys). + Post(bind(api.CreateKeyOption{}), user.CreatePublicKey) + m.Combo("/:id").Get(user.GetPublicKey). + Delete(user.DeletePublicKey) + }) + + m.Combo("/issues").Get(repo.ListUserIssues) + }, reqToken()) + + // Repositories + m.Get("/users/:username/repos", reqToken(), repo.ListUserRepositories) + m.Get("/orgs/:org/repos", reqToken(), repo.ListOrgRepositories) + m.Combo("/user/repos", reqToken()).Get(repo.ListMyRepos). + Post(bind(api.CreateRepoOption{}), repo.Create) + m.Post("/org/:org/repos", reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo) + + m.Group("/repos", func() { + m.Get("/search", repo.Search) + }) + + m.Group("/repos", func() { + m.Post("/migrate", bind(form.MigrateRepo{}), repo.Migrate) + m.Combo("/:username/:reponame", repoAssignment()).Get(repo.Get). + Delete(repo.Delete) + + m.Group("/:username/:reponame", func() { + m.Group("/hooks", func() { + m.Combo("").Get(repo.ListHooks). + Post(bind(api.CreateHookOption{}), repo.CreateHook) + m.Combo("/:id").Patch(bind(api.EditHookOption{}), repo.EditHook). + Delete(repo.DeleteHook) + }) + m.Group("/collaborators", func() { + m.Get("", repo.ListCollaborators) + m.Combo("/:collaborator").Get(repo.IsCollaborator).Put(bind(api.AddCollaboratorOption{}), repo.AddCollaborator). + Delete(repo.DeleteCollaborator) + }) + m.Get("/raw/*", context.RepoRef(), repo.GetRawFile) + m.Get("/archive/*", repo.GetArchive) + m.Get("/forks", repo.ListForks) + m.Group("/branches", func() { + m.Get("", repo.ListBranches) + m.Get("/*", repo.GetBranch) + }) + m.Group("/keys", func() { + m.Combo("").Get(repo.ListDeployKeys). + Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey) + m.Combo("/:id").Get(repo.GetDeployKey). + Delete(repo.DeleteDeploykey) + }) + m.Group("/issues", func() { + m.Combo("").Get(repo.ListIssues).Post(bind(api.CreateIssueOption{}), repo.CreateIssue) + m.Group("/comments", func() { + m.Get("", repo.ListRepoIssueComments) + m.Combo("/:id").Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueComment) + }) + m.Group("/:index", func() { + m.Combo("").Get(repo.GetIssue).Patch(bind(api.EditIssueOption{}), repo.EditIssue) + + m.Group("/comments", func() { + m.Combo("").Get(repo.ListIssueComments).Post(bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment) + m.Combo("/:id").Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueComment). + Delete(repo.DeleteIssueComment) + }) + + m.Group("/labels", func() { + m.Combo("").Get(repo.ListIssueLabels). + Post(bind(api.IssueLabelsOption{}), repo.AddIssueLabels). + Put(bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels). + Delete(repo.ClearIssueLabels) + m.Delete("/:id", repo.DeleteIssueLabel) + }) + + }) + }, mustEnableIssues) + m.Group("/labels", func() { + m.Combo("").Get(repo.ListLabels). + Post(bind(api.CreateLabelOption{}), repo.CreateLabel) + m.Combo("/:id").Get(repo.GetLabel).Patch(bind(api.EditLabelOption{}), repo.EditLabel). + Delete(repo.DeleteLabel) + }) + m.Group("/milestones", func() { + m.Combo("").Get(repo.ListMilestones). + Post(reqRepoWriter(), bind(api.CreateMilestoneOption{}), repo.CreateMilestone) + m.Combo("/:id").Get(repo.GetMilestone). + Patch(reqRepoWriter(), bind(api.EditMilestoneOption{}), repo.EditMilestone). + Delete(reqRepoWriter(), repo.DeleteMilestone) + }) + m.Post("/mirror-sync", repo.MirrorSync) + m.Get("/editorconfig/:filename", context.RepoRef(), repo.GetEditorconfig) + }, repoAssignment()) + }, reqToken()) + + m.Get("/issues", reqToken(), repo.ListUserIssues) + + // Organizations + m.Get("/user/orgs", reqToken(), org.ListMyOrgs) + m.Get("/users/:username/orgs", org.ListUserOrgs) + m.Group("/orgs/:orgname", func() { + m.Combo("").Get(org.Get).Patch(bind(api.EditOrgOption{}), org.Edit) + m.Combo("/teams").Get(org.ListTeams) + }, orgAssignment(true)) + + m.Any("/*", func(c *context.Context) { + c.Error(404) + }) + + m.Group("/admin", func() { + m.Group("/users", func() { + m.Post("", bind(api.CreateUserOption{}), admin.CreateUser) + + m.Group("/:username", func() { + m.Combo("").Patch(bind(api.EditUserOption{}), admin.EditUser). + Delete(admin.DeleteUser) + m.Post("/keys", bind(api.CreateKeyOption{}), admin.CreatePublicKey) + m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg) + m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo) + }) + }) + + m.Group("/orgs/:orgname", func() { + m.Group("/teams", func() { + m.Post("", orgAssignment(true), bind(api.CreateTeamOption{}), admin.CreateTeam) + }) + }) + m.Group("/teams", func() { + m.Group("/:teamid", func() { + m.Combo("/members/:username").Put(admin.AddTeamMember).Delete(admin.RemoveTeamMember) + m.Combo("/repos/:reponame").Put(admin.AddTeamRepository).Delete(admin.RemoveTeamRepository) + }, orgAssignment(false, true)) + }) + }, reqAdmin()) + }, context.APIContexter()) +} diff --git a/routes/api/v1/convert/convert.go b/routes/api/v1/convert/convert.go new file mode 100644 index 00000000..fcadb51f --- /dev/null +++ b/routes/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/gogits/git-module" + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" +) + +func ToEmail(email *models.EmailAddress) *api.Email { + return &api.Email{ + Email: email.Email, + Verified: email.IsActivated, + Primary: email.IsPrimary, + } +} + +func ToBranch(b *models.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 := models.GetUserByEmail(c.Author.Email) + if err == nil { + authorUsername = author.Name + } + committerUsername := "" + committer, err := models.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 *models.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 *models.Webhook) *api.Hook { + config := map[string]string{ + "url": w.URL, + "content_type": w.ContentType.Name(), + } + if w.HookTaskType == models.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 *models.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 *models.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 *models.Team) *api.Team { + return &api.Team{ + ID: team.ID, + Name: team.Name, + Description: team.Description, + Permission: team.Authorize.String(), + } +} diff --git a/routes/api/v1/convert/utils.go b/routes/api/v1/convert/utils.go new file mode 100644 index 00000000..d0beab3d --- /dev/null +++ b/routes/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 ( + "github.com/gogits/gogs/pkg/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/routes/api/v1/misc/markdown.go b/routes/api/v1/misc/markdown.go new file mode 100644 index 00000000..98bfd7d0 --- /dev/null +++ b/routes/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 ( + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/markup" +) + +// https://github.com/gogits/go-gogs-client/wiki/Miscellaneous#render-an-arbitrary-markdown-document +func Markdown(c *context.APIContext, form api.MarkdownOption) { + if c.HasApiError() { + c.Error(422, "", 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), "")) + } +} + +// https://github.com/gogits/go-gogs-client/wiki/Miscellaneous#render-a-markdown-document-in-raw-mode +func MarkdownRaw(c *context.APIContext) { + body, err := c.Req.Body().Bytes() + if err != nil { + c.Error(422, "", err) + return + } + c.Write(markup.RawMarkdown(body, "")) +} diff --git a/routes/api/v1/org/org.go b/routes/api/v1/org/org.go new file mode 100644 index 00000000..2f8832ca --- /dev/null +++ b/routes/api/v1/org/org.go @@ -0,0 +1,66 @@ +// 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 ( + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/routes/api/v1/convert" + "github.com/gogits/gogs/routes/api/v1/user" +) + +func listUserOrgs(c *context.APIContext, u *models.User, all bool) { + if err := u.GetOrganizations(all); err != nil { + c.Error(500, "GetOrganizations", err) + return + } + + apiOrgs := make([]*api.Organization, len(u.Orgs)) + for i := range u.Orgs { + apiOrgs[i] = convert.ToOrganization(u.Orgs[i]) + } + c.JSON(200, &apiOrgs) +} + +// https://github.com/gogits/go-gogs-client/wiki/Organizations#list-your-organizations +func ListMyOrgs(c *context.APIContext) { + listUserOrgs(c, c.User, true) +} + +// https://github.com/gogits/go-gogs-client/wiki/Organizations#list-user-organizations +func ListUserOrgs(c *context.APIContext) { + u := user.GetUserByParams(c) + if c.Written() { + return + } + listUserOrgs(c, u, false) +} + +// https://github.com/gogits/go-gogs-client/wiki/Organizations#get-an-organization +func Get(c *context.APIContext) { + c.JSON(200, convert.ToOrganization(c.Org.Organization)) +} + +// https://github.com/gogits/go-gogs-client/wiki/Organizations#edit-an-organization +func Edit(c *context.APIContext, form api.EditOrgOption) { + org := c.Org.Organization + if !org.IsOwnedBy(c.User.ID) { + c.Status(403) + return + } + + org.FullName = form.FullName + org.Description = form.Description + org.Website = form.Website + org.Location = form.Location + if err := models.UpdateUser(org); err != nil { + c.Error(500, "UpdateUser", err) + return + } + + c.JSON(200, convert.ToOrganization(org)) +} diff --git a/routes/api/v1/org/team.go b/routes/api/v1/org/team.go new file mode 100644 index 00000000..fd92e728 --- /dev/null +++ b/routes/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/gogits/go-gogs-client" + + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/routes/api/v1/convert" +) + +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] = convert.ToTeam(org.Teams[i]) + } + c.JSON(200, apiTeams) +} diff --git a/routes/api/v1/repo/branch.go b/routes/api/v1/repo/branch.go new file mode 100644 index 00000000..d8c2697b --- /dev/null +++ b/routes/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/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/routes/api/v1/convert" +) + +// https://github.com/gogits/go-gogs-client/wiki/Repositories#get-branch +func GetBranch(c *context.APIContext) { + branch, err := c.Repo.Repository.GetBranch(c.Params("*")) + if err != nil { + if models.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, convert.ToBranch(branch, commit)) +} + +// https://github.com/gogits/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] = convert.ToBranch(branches[i], commit) + } + + c.JSON(200, &apiBranches) +} diff --git a/routes/api/v1/repo/collaborators.go b/routes/api/v1/repo/collaborators.go new file mode 100644 index 00000000..d295ac0f --- /dev/null +++ b/routes/api/v1/repo/collaborators.go @@ -0,0 +1,94 @@ +// 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/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" +) + +func ListCollaborators(c *context.APIContext) { + collaborators, err := c.Repo.Repository.GetCollaborators() + if err != nil { + if errors.IsUserNotExist(err) { + c.Error(422, "", err) + } else { + c.Error(500, "GetCollaborators", err) + } + return + } + + apiCollaborators := make([]*api.Collaborator, len(collaborators)) + for i := range collaborators { + apiCollaborators[i] = collaborators[i].APIFormat() + } + c.JSON(200, &apiCollaborators) +} + +func AddCollaborator(c *context.APIContext, form api.AddCollaboratorOption) { + collaborator, err := models.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, models.ParseAccessMode(*form.Permission)); err != nil { + c.Error(500, "ChangeCollaborationAccessMode", err) + return + } + } + + c.Status(204) +} + +func IsCollaborator(c *context.APIContext) { + collaborator, err := models.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 := models.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/routes/api/v1/repo/file.go b/routes/api/v1/repo/file.go new file mode 100644 index 00000000..c783e81f --- /dev/null +++ b/routes/api/v1/repo/file.go @@ -0,0 +1,72 @@ +// 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/gogits/git-module" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/routes/repo" +) + +// https://github.com/gogits/go-gogs-client/wiki/Repositories-Contents#download-raw-content +func GetRawFile(c *context.APIContext) { + if !c.Repo.HasAccess() { + c.Status(404) + return + } + + if c.Repo.Repository.IsBare { + c.Status(404) + return + } + + blob, err := c.Repo.Commit.GetBlobByPath(c.Repo.TreePath) + if err != nil { + if git.IsErrNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetBlobByPath", err) + } + return + } + if err = repo.ServeBlob(c.Context, blob); err != nil { + c.Error(500, "ServeBlob", err) + } +} + +// https://github.com/gogits/go-gogs-client/wiki/Repositories-Contents#download-archive +func GetArchive(c *context.APIContext) { + repoPath := models.RepoPath(c.Params(":username"), c.Params(":reponame")) + gitRepo, err := git.OpenRepository(repoPath) + if err != nil { + c.Error(500, "OpenRepository", err) + return + } + c.Repo.GitRepo = gitRepo + + repo.Download(c.Context) +} + +func GetEditorconfig(c *context.APIContext) { + ec, err := c.Repo.GetEditorconfig() + if err != nil { + if git.IsErrNotExist(err) { + c.Error(404, "GetEditorconfig", err) + } else { + c.Error(500, "GetEditorconfig", err) + } + return + } + + fileName := c.Params("filename") + def := ec.GetDefinitionForFilename(fileName) + if def == nil { + c.Error(404, "GetDefinitionForFilename", err) + return + } + c.JSON(200, def) +} diff --git a/routes/api/v1/repo/hook.go b/routes/api/v1/repo/hook.go new file mode 100644 index 00000000..66125c50 --- /dev/null +++ b/routes/api/v1/repo/hook.go @@ -0,0 +1,186 @@ +// 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 ( + "encoding/json" + + "github.com/Unknwon/com" + + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/routes/api/v1/convert" +) + +// https://github.com/gogits/go-gogs-client/wiki/Repositories#list-hooks +func ListHooks(c *context.APIContext) { + hooks, err := models.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] = convert.ToHook(c.Repo.RepoLink, hooks[i]) + } + c.JSON(200, &apiHooks) +} + +// https://github.com/gogits/go-gogs-client/wiki/Repositories#create-a-hook +func CreateHook(c *context.APIContext, form api.CreateHookOption) { + if !models.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 !models.IsValidHookContentType(form.Config["content_type"]) { + c.Error(422, "", "Invalid content type") + return + } + + if len(form.Events) == 0 { + form.Events = []string{"push"} + } + w := &models.Webhook{ + RepoID: c.Repo.Repository.ID, + URL: form.Config["url"], + ContentType: models.ToHookContentType(form.Config["content_type"]), + Secret: form.Config["secret"], + HookEvent: &models.HookEvent{ + ChooseEvents: true, + HookEvents: models.HookEvents{ + Create: com.IsSliceContainsStr(form.Events, string(models.HOOK_EVENT_CREATE)), + Delete: com.IsSliceContainsStr(form.Events, string(models.HOOK_EVENT_DELETE)), + Fork: com.IsSliceContainsStr(form.Events, string(models.HOOK_EVENT_FORK)), + Push: com.IsSliceContainsStr(form.Events, string(models.HOOK_EVENT_PUSH)), + Issues: com.IsSliceContainsStr(form.Events, string(models.HOOK_EVENT_ISSUES)), + IssueComment: com.IsSliceContainsStr(form.Events, string(models.HOOK_EVENT_ISSUE_COMMENT)), + PullRequest: com.IsSliceContainsStr(form.Events, string(models.HOOK_EVENT_PULL_REQUEST)), + Release: com.IsSliceContainsStr(form.Events, string(models.HOOK_EVENT_RELEASE)), + }, + }, + IsActive: form.Active, + HookTaskType: models.ToHookTaskType(form.Type), + } + if w.HookTaskType == models.SLACK { + channel, ok := form.Config["channel"] + if !ok { + c.Error(422, "", "Missing config option: channel") + return + } + meta, err := json.Marshal(&models.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 := models.CreateWebhook(w); err != nil { + c.Error(500, "CreateWebhook", err) + return + } + + c.JSON(201, convert.ToHook(c.Repo.RepoLink, w)) +} + +// https://github.com/gogits/go-gogs-client/wiki/Repositories#edit-a-hook +func EditHook(c *context.APIContext, form api.EditHookOption) { + w, err := models.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 !models.IsValidHookContentType(ct) { + c.Error(422, "", "Invalid content type") + return + } + w.ContentType = models.ToHookContentType(ct) + } + + if w.HookTaskType == models.SLACK { + if channel, ok := form.Config["channel"]; ok { + meta, err := json.Marshal(&models.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(models.HOOK_EVENT_CREATE)) + w.Delete = com.IsSliceContainsStr(form.Events, string(models.HOOK_EVENT_DELETE)) + w.Fork = com.IsSliceContainsStr(form.Events, string(models.HOOK_EVENT_FORK)) + w.Push = com.IsSliceContainsStr(form.Events, string(models.HOOK_EVENT_PUSH)) + w.Issues = com.IsSliceContainsStr(form.Events, string(models.HOOK_EVENT_ISSUES)) + w.IssueComment = com.IsSliceContainsStr(form.Events, string(models.HOOK_EVENT_ISSUE_COMMENT)) + w.PullRequest = com.IsSliceContainsStr(form.Events, string(models.HOOK_EVENT_PULL_REQUEST)) + w.Release = com.IsSliceContainsStr(form.Events, string(models.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 := models.UpdateWebhook(w); err != nil { + c.Error(500, "UpdateWebhook", err) + return + } + + c.JSON(200, convert.ToHook(c.Repo.RepoLink, w)) +} + +func DeleteHook(c *context.APIContext) { + if err := models.DeleteWebhookOfRepoByID(c.Repo.Repository.ID, c.ParamsInt64(":id")); err != nil { + c.Error(500, "DeleteWebhookByRepoID", err) + return + } + + c.Status(204) +} diff --git a/routes/api/v1/repo/issue.go b/routes/api/v1/repo/issue.go new file mode 100644 index 00000000..d6ae7b4d --- /dev/null +++ b/routes/api/v1/repo/issue.go @@ -0,0 +1,201 @@ +// Copyright 2016 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package repo + +import ( + "fmt" + "strings" + + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" +) + +func listIssues(c *context.APIContext, opts *models.IssuesOptions) { + issues, err := models.Issues(opts) + if err != nil { + c.Error(500, "Issues", err) + return + } + + count, err := models.IssuesCount(opts) + if err != nil { + c.Error(500, "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.Error(500, "LoadAttributes", err) + return + } + apiIssues[i] = issues[i].APIFormat() + } + + c.SetLinkHeader(int(count), setting.UI.IssuePagingNum) + c.JSON(200, &apiIssues) +} + +func ListUserIssues(c *context.APIContext) { + opts := models.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 := models.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 := models.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + if errors.IsIssueNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetIssueByIndex", err) + } + return + } + c.JSON(200, issue.APIFormat()) +} + +func CreateIssue(c *context.APIContext, form api.CreateIssueOption) { + issue := &models.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 := models.GetUserByName(form.Assignee) + if err != nil { + if errors.IsUserNotExist(err) { + c.Error(422, "", fmt.Sprintf("Assignee does not exist: [name: %s]", form.Assignee)) + } else { + c.Error(500, "GetUserByName", err) + } + return + } + issue.AssigneeID = assignee.ID + } + issue.MilestoneID = form.Milestone + } else { + form.Labels = nil + } + + if err := models.NewIssue(c.Repo.Repository, issue, form.Labels, nil); err != nil { + c.Error(500, "NewIssue", err) + return + } + + if form.Closed { + if err := issue.ChangeStatus(c.User, c.Repo.Repository, true); err != nil { + c.Error(500, "ChangeStatus", err) + return + } + } + + // Refetch from database to assign some automatic values + var err error + issue, err = models.GetIssueByID(issue.ID) + if err != nil { + c.Error(500, "GetIssueByID", err) + return + } + c.JSON(201, issue.APIFormat()) +} + +func EditIssue(c *context.APIContext, form api.EditIssueOption) { + issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + if errors.IsIssueNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetIssueByIndex", err) + } + return + } + + if !issue.IsPoster(c.User.ID) && !c.Repo.IsWriter() { + c.Status(403) + 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 := models.GetUserByName(*form.Assignee) + if err != nil { + if errors.IsUserNotExist(err) { + c.Error(422, "", fmt.Sprintf("assignee does not exist: [name: %s]", *form.Assignee)) + } else { + c.Error(500, "GetUserByName", err) + } + return + } + issue.AssigneeID = assignee.ID + } + + if err = models.UpdateIssueUserByAssignee(issue); err != nil { + c.Error(500, "UpdateIssueUserByAssignee", err) + return + } + } + if c.Repo.IsWriter() && form.Milestone != nil && + issue.MilestoneID != *form.Milestone { + oldMilestoneID := issue.MilestoneID + issue.MilestoneID = *form.Milestone + if err = models.ChangeMilestoneAssign(c.User, issue, oldMilestoneID); err != nil { + c.Error(500, "ChangeMilestoneAssign", err) + return + } + } + + if err = models.UpdateIssue(issue); err != nil { + c.Error(500, "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.Error(500, "ChangeStatus", err) + return + } + } + + // Refetch from database to assign some automatic values + issue, err = models.GetIssueByID(issue.ID) + if err != nil { + c.Error(500, "GetIssueByID", err) + return + } + c.JSON(201, issue.APIFormat()) +} diff --git a/routes/api/v1/repo/issue_comment.go b/routes/api/v1/repo/issue_comment.go new file mode 100644 index 00000000..4a057d76 --- /dev/null +++ b/routes/api/v1/repo/issue_comment.go @@ -0,0 +1,128 @@ +// 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 ( + "time" + + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" +) + +func ListIssueComments(c *context.APIContext) { + var since time.Time + if len(c.Query("since")) > 0 { + since, _ = time.Parse(time.RFC3339, c.Query("since")) + } + + // comments,err:=models.GetCommentsByIssueIDSince(, since) + issue, err := models.GetRawIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + c.Error(500, "GetRawIssueByIndex", err) + return + } + + comments, err := models.GetCommentsByIssueIDSince(issue.ID, since.Unix()) + if err != nil { + c.Error(500, "GetCommentsByIssueIDSince", err) + return + } + + apiComments := make([]*api.Comment, len(comments)) + for i := range comments { + apiComments[i] = comments[i].APIFormat() + } + c.JSON(200, &apiComments) +} + +func ListRepoIssueComments(c *context.APIContext) { + var since time.Time + if len(c.Query("since")) > 0 { + since, _ = time.Parse(time.RFC3339, c.Query("since")) + } + + comments, err := models.GetCommentsByRepoIDSince(c.Repo.Repository.ID, since.Unix()) + if err != nil { + c.Error(500, "GetCommentsByRepoIDSince", err) + return + } + + apiComments := make([]*api.Comment, len(comments)) + for i := range comments { + apiComments[i] = comments[i].APIFormat() + } + c.JSON(200, &apiComments) +} + +func CreateIssueComment(c *context.APIContext, form api.CreateIssueCommentOption) { + issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + c.Error(500, "GetIssueByIndex", err) + return + } + + comment, err := models.CreateIssueComment(c.User, c.Repo.Repository, issue, form.Body, nil) + if err != nil { + c.Error(500, "CreateIssueComment", err) + return + } + + c.JSON(201, comment.APIFormat()) +} + +func EditIssueComment(c *context.APIContext, form api.EditIssueCommentOption) { + comment, err := models.GetCommentByID(c.ParamsInt64(":id")) + if err != nil { + if models.IsErrCommentNotExist(err) { + c.Error(404, "GetCommentByID", err) + } else { + c.Error(500, "GetCommentByID", err) + } + return + } + + if c.User.ID != comment.PosterID && !c.Repo.IsAdmin() { + c.Status(403) + return + } else if comment.Type != models.COMMENT_TYPE_COMMENT { + c.Status(204) + return + } + + oldContent := comment.Content + comment.Content = form.Body + if err := models.UpdateComment(c.User, comment, oldContent); err != nil { + c.Error(500, "UpdateComment", err) + return + } + c.JSON(200, comment.APIFormat()) +} + +func DeleteIssueComment(c *context.APIContext) { + comment, err := models.GetCommentByID(c.ParamsInt64(":id")) + if err != nil { + if models.IsErrCommentNotExist(err) { + c.Error(404, "GetCommentByID", err) + } else { + c.Error(500, "GetCommentByID", err) + } + return + } + + if c.User.ID != comment.PosterID && !c.Repo.IsAdmin() { + c.Status(403) + return + } else if comment.Type != models.COMMENT_TYPE_COMMENT { + c.Status(204) + return + } + + if err = models.DeleteCommentByID(c.User, comment.ID); err != nil { + c.Error(500, "DeleteCommentByID", err) + return + } + c.Status(204) +} diff --git a/routes/api/v1/repo/issue_label.go b/routes/api/v1/repo/issue_label.go new file mode 100644 index 00000000..f3f2d730 --- /dev/null +++ b/routes/api/v1/repo/issue_label.go @@ -0,0 +1,169 @@ +// 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/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" +) + +func ListIssueLabels(c *context.APIContext) { + issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + if errors.IsIssueNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetIssueByIndex", err) + } + return + } + + apiLabels := make([]*api.Label, len(issue.Labels)) + for i := range issue.Labels { + apiLabels[i] = issue.Labels[i].APIFormat() + } + c.JSON(200, &apiLabels) +} + +func AddIssueLabels(c *context.APIContext, form api.IssueLabelsOption) { + if !c.Repo.IsWriter() { + c.Status(403) + return + } + + issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + if errors.IsIssueNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetIssueByIndex", err) + } + return + } + + labels, err := models.GetLabelsInRepoByIDs(c.Repo.Repository.ID, form.Labels) + if err != nil { + c.Error(500, "GetLabelsInRepoByIDs", err) + return + } + + if err = issue.AddLabels(c.User, labels); err != nil { + c.Error(500, "AddLabels", err) + return + } + + labels, err = models.GetLabelsByIssueID(issue.ID) + if err != nil { + c.Error(500, "GetLabelsByIssueID", err) + return + } + + apiLabels := make([]*api.Label, len(labels)) + for i := range labels { + apiLabels[i] = issue.Labels[i].APIFormat() + } + c.JSON(200, &apiLabels) +} + +func DeleteIssueLabel(c *context.APIContext) { + if !c.Repo.IsWriter() { + c.Status(403) + return + } + + issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + if errors.IsIssueNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetIssueByIndex", err) + } + return + } + + label, err := models.GetLabelOfRepoByID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + if models.IsErrLabelNotExist(err) { + c.Error(422, "", err) + } else { + c.Error(500, "GetLabelInRepoByID", err) + } + return + } + + if err := models.DeleteIssueLabel(issue, label); err != nil { + c.Error(500, "DeleteIssueLabel", err) + return + } + + c.Status(204) +} + +func ReplaceIssueLabels(c *context.APIContext, form api.IssueLabelsOption) { + if !c.Repo.IsWriter() { + c.Status(403) + return + } + + issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + if errors.IsIssueNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetIssueByIndex", err) + } + return + } + + labels, err := models.GetLabelsInRepoByIDs(c.Repo.Repository.ID, form.Labels) + if err != nil { + c.Error(500, "GetLabelsInRepoByIDs", err) + return + } + + if err := issue.ReplaceLabels(labels); err != nil { + c.Error(500, "ReplaceLabels", err) + return + } + + labels, err = models.GetLabelsByIssueID(issue.ID) + if err != nil { + c.Error(500, "GetLabelsByIssueID", err) + return + } + + apiLabels := make([]*api.Label, len(labels)) + for i := range labels { + apiLabels[i] = issue.Labels[i].APIFormat() + } + c.JSON(200, &apiLabels) +} + +func ClearIssueLabels(c *context.APIContext) { + if !c.Repo.IsWriter() { + c.Status(403) + return + } + + issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + if errors.IsIssueNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetIssueByIndex", err) + } + return + } + + if err := issue.ClearLabels(c.User); err != nil { + c.Error(500, "ClearLabels", err) + return + } + + c.Status(204) +} diff --git a/routes/api/v1/repo/key.go b/routes/api/v1/repo/key.go new file mode 100644 index 00000000..405440d2 --- /dev/null +++ b/routes/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" + + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/routes/api/v1/convert" +) + +func composeDeployKeysAPILink(repoPath string) string { + return setting.AppURL + "api/v1/repos/" + repoPath + "/keys/" +} + +// https://github.com/gogits/go-gogs-client/wiki/Repositories-Deploy-Keys#list-deploy-keys +func ListDeployKeys(c *context.APIContext) { + keys, err := models.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] = convert.ToDeployKey(apiLink, keys[i]) + } + + c.JSON(200, &apiKeys) +} + +// https://github.com/gogits/go-gogs-client/wiki/Repositories-Deploy-Keys#get-a-deploy-key +func GetDeployKey(c *context.APIContext) { + key, err := models.GetDeployKeyByID(c.ParamsInt64(":id")) + if err != nil { + if models.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, convert.ToDeployKey(apiLink, key)) +} + +func HandleCheckKeyStringError(c *context.APIContext, err error) { + if models.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 models.IsErrKeyAlreadyExist(err): + c.Error(422, "", "Key content has been used as non-deploy key") + case models.IsErrKeyNameAlreadyUsed(err): + c.Error(422, "", "Key title has been used") + default: + c.Error(500, "AddKey", err) + } +} + +// https://github.com/gogits/go-gogs-client/wiki/Repositories-Deploy-Keys#add-a-new-deploy-key +func CreateDeployKey(c *context.APIContext, form api.CreateKeyOption) { + content, err := models.CheckPublicKeyString(form.Key) + if err != nil { + HandleCheckKeyStringError(c, err) + return + } + + key, err := models.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, convert.ToDeployKey(apiLink, key)) +} + +// https://github.com/gogits/go-gogs-client/wiki/Repositories-Deploy-Keys#remove-a-deploy-key +func DeleteDeploykey(c *context.APIContext) { + if err := models.DeleteDeployKey(c.User, c.ParamsInt64(":id")); err != nil { + if models.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/routes/api/v1/repo/label.go b/routes/api/v1/repo/label.go new file mode 100644 index 00000000..1161d633 --- /dev/null +++ b/routes/api/v1/repo/label.go @@ -0,0 +1,110 @@ +// 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 ( + "github.com/Unknwon/com" + + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" +) + +func ListLabels(c *context.APIContext) { + labels, err := models.GetLabelsByRepoID(c.Repo.Repository.ID) + if err != nil { + c.Error(500, "GetLabelsByRepoID", err) + return + } + + apiLabels := make([]*api.Label, len(labels)) + for i := range labels { + apiLabels[i] = labels[i].APIFormat() + } + c.JSON(200, &apiLabels) +} + +func GetLabel(c *context.APIContext) { + var label *models.Label + var err error + idStr := c.Params(":id") + if id := com.StrTo(idStr).MustInt64(); id > 0 { + label, err = models.GetLabelOfRepoByID(c.Repo.Repository.ID, id) + } else { + label, err = models.GetLabelOfRepoByName(c.Repo.Repository.ID, idStr) + } + if err != nil { + if models.IsErrLabelNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetLabelByRepoID", err) + } + return + } + + c.JSON(200, label.APIFormat()) +} + +func CreateLabel(c *context.APIContext, form api.CreateLabelOption) { + if !c.Repo.IsWriter() { + c.Status(403) + return + } + + label := &models.Label{ + Name: form.Name, + Color: form.Color, + RepoID: c.Repo.Repository.ID, + } + if err := models.NewLabels(label); err != nil { + c.Error(500, "NewLabel", err) + return + } + c.JSON(201, label.APIFormat()) +} + +func EditLabel(c *context.APIContext, form api.EditLabelOption) { + if !c.Repo.IsWriter() { + c.Status(403) + return + } + + label, err := models.GetLabelOfRepoByID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + if models.IsErrLabelNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetLabelByRepoID", err) + } + return + } + + if form.Name != nil { + label.Name = *form.Name + } + if form.Color != nil { + label.Color = *form.Color + } + if err := models.UpdateLabel(label); err != nil { + c.Handle(500, "UpdateLabel", err) + return + } + c.JSON(200, label.APIFormat()) +} + +func DeleteLabel(c *context.APIContext) { + if !c.Repo.IsWriter() { + c.Status(403) + return + } + + if err := models.DeleteLabel(c.Repo.Repository.ID, c.ParamsInt64(":id")); err != nil { + c.Error(500, "DeleteLabel", err) + return + } + + c.Status(204) +} diff --git a/routes/api/v1/repo/milestone.go b/routes/api/v1/repo/milestone.go new file mode 100644 index 00000000..baf8eb2f --- /dev/null +++ b/routes/api/v1/repo/milestone.go @@ -0,0 +1,103 @@ +// 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 ( + "time" + + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" +) + +func ListMilestones(c *context.APIContext) { + milestones, err := models.GetMilestonesByRepoID(c.Repo.Repository.ID) + if err != nil { + c.Error(500, "GetMilestonesByRepoID", err) + return + } + + apiMilestones := make([]*api.Milestone, len(milestones)) + for i := range milestones { + apiMilestones[i] = milestones[i].APIFormat() + } + c.JSON(200, &apiMilestones) +} + +func GetMilestone(c *context.APIContext) { + milestone, err := models.GetMilestoneByRepoID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + if models.IsErrMilestoneNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetMilestoneByRepoID", err) + } + return + } + c.JSON(200, 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 := &models.Milestone{ + RepoID: c.Repo.Repository.ID, + Name: form.Title, + Content: form.Description, + Deadline: *form.Deadline, + } + + if err := models.NewMilestone(milestone); err != nil { + c.Error(500, "NewMilestone", err) + return + } + c.JSON(201, milestone.APIFormat()) +} + +func EditMilestone(c *context.APIContext, form api.EditMilestoneOption) { + milestone, err := models.GetMilestoneByRepoID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + if models.IsErrMilestoneNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetMilestoneByRepoID", 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.Error(500, "ChangeStatus", err) + return + } + } else if err = models.UpdateMilestone(milestone); err != nil { + c.Handle(500, "UpdateMilestone", err) + return + } + + c.JSON(200, milestone.APIFormat()) +} + +func DeleteMilestone(c *context.APIContext) { + if err := models.DeleteMilestoneOfRepoByID(c.Repo.Repository.ID, c.ParamsInt64(":id")); err != nil { + c.Error(500, "DeleteMilestoneByRepoID", err) + return + } + c.Status(204) +} diff --git a/routes/api/v1/repo/repo.go b/routes/api/v1/repo/repo.go new file mode 100644 index 00000000..8410dcca --- /dev/null +++ b/routes/api/v1/repo/repo.go @@ -0,0 +1,380 @@ +// 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 ( + "path" + + log "gopkg.in/clog.v1" + + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/routes/api/v1/convert" +) + +// https://github.com/gogits/go-gogs-client/wiki/Repositories#search-repositories +func Search(c *context.APIContext) { + opts := &models.SearchRepoOptions{ + Keyword: path.Base(c.Query("q")), + OwnerID: c.QueryInt64("uid"), + PageSize: convert.ToCorrectPageSize(c.QueryInt("limit")), + } + + // Check visibility. + if c.IsLogged && opts.OwnerID > 0 { + if c.User.ID == opts.OwnerID { + opts.Private = true + } else { + u, err := models.GetUserByID(opts.OwnerID) + if err != nil { + c.JSON(500, 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 := models.SearchRepositoryByName(opts) + if err != nil { + c.JSON(500, map[string]interface{}{ + "ok": false, + "error": err.Error(), + }) + return + } + + if err = models.RepositoryList(repos).LoadAttributes(); err != nil { + c.JSON(500, 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), setting.API.MaxResponseItems) + c.JSON(200, map[string]interface{}{ + "ok": true, + "data": results, + }) +} + +func listUserRepositories(c *context.APIContext, username string) { + user, err := models.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 []*models.Repository + if user.IsOrganization() { + ownRepos, _, err = user.GetUserRepositories(c.User.ID, 1, user.NumRepos) + } else { + ownRepos, err = models.GetUserRepositories(&models.UserRepoOptions{ + UserID: user.ID, + Private: c.User.ID == user.ID, + Page: 1, + PageSize: user.NumRepos, + }) + } + if err != nil { + c.Error(500, "GetUserRepositories", err) + return + } + + 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.JSON(200, &repos) + return + } + + accessibleRepos, err := user.GetRepositoryAccesses() + if err != nil { + c.Error(500, "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 >= models.ACCESS_MODE_ADMIN, + Push: access >= models.ACCESS_MODE_WRITE, + Pull: true, + }) + i++ + } + + c.JSON(200, &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 *models.User, opt api.CreateRepoOption) { + repo, err := models.CreateRepository(c.User, owner, models.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 models.IsErrRepoAlreadyExist(err) || + models.IsErrNameReserved(err) || + models.IsErrNamePatternNotAllowed(err) { + c.Error(422, "", err) + } else { + if repo != nil { + if err = models.DeleteRepository(c.User.ID, repo.ID); err != nil { + log.Error(2, "DeleteRepository: %v", err) + } + } + c.Error(500, "CreateRepository", err) + } + return + } + + c.JSON(201, repo.APIFormat(&api.Permission{true, true, true})) +} + +// https://github.com/gogits/go-gogs-client/wiki/Repositories#create +func Create(c *context.APIContext, opt api.CreateRepoOption) { + // Shouldn't reach this condition, but just in case. + if c.User.IsOrganization() { + c.Error(422, "", "not allowed creating repository for organization") + return + } + CreateUserRepo(c, c.User, opt) +} + +func CreateOrgRepo(c *context.APIContext, opt api.CreateRepoOption) { + org, err := models.GetOrgByName(c.Params(":org")) + if err != nil { + if errors.IsUserNotExist(err) { + c.Error(422, "", err) + } else { + c.Error(500, "GetOrgByName", err) + } + return + } + + if !org.IsOwnedBy(c.User.ID) { + c.Error(403, "", "Given user is not owner of organization.") + return + } + CreateUserRepo(c, org, opt) +} + +// https://github.com/gogits/go-gogs-client/wiki/Repositories#migrate +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 := models.GetUserByID(f.Uid) + if err != nil { + if errors.IsUserNotExist(err) { + c.Error(422, "", err) + } else { + c.Error(500, "GetUserByID", err) + } + return + } else if !org.IsOrganization() && !c.User.IsAdmin { + c.Error(403, "", "Given user is not an organization") + return + } + ctxUser = org + } + + if c.HasError() { + c.Error(422, "", c.GetErrMsg()) + return + } + + if ctxUser.IsOrganization() && !c.User.IsAdmin { + // Check ownership of organization. + if !ctxUser.IsOwnedBy(c.User.ID) { + c.Error(403, "", "Given user is not owner of organization") + return + } + } + + remoteAddr, err := f.ParseRemoteAddr(c.User) + if err != nil { + if models.IsErrInvalidCloneAddr(err) { + addrErr := err.(models.ErrInvalidCloneAddr) + switch { + case addrErr.IsURLError: + c.Error(422, "", err) + case addrErr.IsPermissionDenied: + c.Error(422, "", "You are not allowed to import local repositories") + case addrErr.IsInvalidPath: + c.Error(422, "", "Invalid local path, it does not exist or not a directory") + default: + c.Error(500, "ParseRemoteAddr", "Unknown error type (ErrInvalidCloneAddr): "+err.Error()) + } + } else { + c.Error(500, "ParseRemoteAddr", err) + } + return + } + + repo, err := models.MigrateRepository(c.User, ctxUser, models.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 := models.DeleteRepository(ctxUser.ID, repo.ID); errDelete != nil { + log.Error(2, "DeleteRepository: %v", errDelete) + } + } + + if errors.IsReachLimitOfRepo(err) { + c.Error(422, "", err) + } else { + c.Error(500, "MigrateRepository", models.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})) +} + +func parseOwnerAndRepo(c *context.APIContext) (*models.User, *models.Repository) { + owner, err := models.GetUserByName(c.Params(":username")) + if err != nil { + if errors.IsUserNotExist(err) { + c.Error(422, "", err) + } else { + c.Error(500, "GetUserByName", err) + } + return nil, nil + } + + repo, err := models.GetRepositoryByName(owner.ID, c.Params(":reponame")) + if err != nil { + if errors.IsRepoNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetRepositoryByName", err) + } + return nil, nil + } + + return owner, repo +} + +// https://github.com/gogits/go-gogs-client/wiki/Repositories#get +func Get(c *context.APIContext) { + _, repo := parseOwnerAndRepo(c) + if c.Written() { + return + } + + c.JSON(200, repo.APIFormat(&api.Permission{ + Admin: c.Repo.IsAdmin(), + Push: c.Repo.IsWriter(), + Pull: true, + })) +} + +// https://github.com/gogits/go-gogs-client/wiki/Repositories#delete +func Delete(c *context.APIContext) { + owner, repo := parseOwnerAndRepo(c) + if c.Written() { + return + } + + if owner.IsOrganization() && !owner.IsOwnedBy(c.User.ID) { + c.Error(403, "", "Given user is not owner of organization.") + return + } + + if err := models.DeleteRepository(owner.ID, repo.ID); err != nil { + c.Error(500, "DeleteRepository", err) + return + } + + log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name) + c.Status(204) +} + +func ListForks(c *context.APIContext) { + forks, err := c.Repo.Repository.GetForks() + if err != nil { + c.Error(500, "GetForks", err) + return + } + + apiForks := make([]*api.Repository, len(forks)) + for i := range forks { + if err := forks[i].GetOwner(); err != nil { + c.Error(500, "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.JSON(200, &apiForks) +} + +func MirrorSync(c *context.APIContext) { + _, repo := parseOwnerAndRepo(c) + if c.Written() { + return + } else if !repo.IsMirror { + c.Status(404) + return + } + + go models.MirrorQueue.Add(repo.ID) + c.Status(202) +} diff --git a/routes/api/v1/user/app.go b/routes/api/v1/user/app.go new file mode 100644 index 00000000..bda1e23f --- /dev/null +++ b/routes/api/v1/user/app.go @@ -0,0 +1,40 @@ +// 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 ( + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" +) + +// https://github.com/gogits/go-gogs-client/wiki/Users#list-access-tokens-for-a-user +func ListAccessTokens(c *context.APIContext) { + tokens, err := models.ListAccessTokens(c.User.ID) + if err != nil { + c.Error(500, "ListAccessTokens", err) + return + } + + apiTokens := make([]*api.AccessToken, len(tokens)) + for i := range tokens { + apiTokens[i] = &api.AccessToken{tokens[i].Name, tokens[i].Sha1} + } + c.JSON(200, &apiTokens) +} + +// https://github.com/gogits/go-gogs-client/wiki/Users#create-a-access-token +func CreateAccessToken(c *context.APIContext, form api.CreateAccessTokenOption) { + t := &models.AccessToken{ + UID: c.User.ID, + Name: form.Name, + } + if err := models.NewAccessToken(t); err != nil { + c.Error(500, "NewAccessToken", err) + return + } + c.JSON(201, &api.AccessToken{t.Name, t.Sha1}) +} diff --git a/routes/api/v1/user/email.go b/routes/api/v1/user/email.go new file mode 100644 index 00000000..bd1ea52b --- /dev/null +++ b/routes/api/v1/user/email.go @@ -0,0 +1,82 @@ +// 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/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/routes/api/v1/convert" +) + +// https://github.com/gogits/go-gogs-client/wiki/Users-Emails#list-email-addresses-for-a-user +func ListEmails(c *context.APIContext) { + emails, err := models.GetEmailAddresses(c.User.ID) + if err != nil { + c.Error(500, "GetEmailAddresses", err) + return + } + apiEmails := make([]*api.Email, len(emails)) + for i := range emails { + apiEmails[i] = convert.ToEmail(emails[i]) + } + c.JSON(200, &apiEmails) +} + +// https://github.com/gogits/go-gogs-client/wiki/Users-Emails#add-email-addresses +func AddEmail(c *context.APIContext, form api.CreateEmailOption) { + if len(form.Emails) == 0 { + c.Status(422) + return + } + + emails := make([]*models.EmailAddress, len(form.Emails)) + for i := range form.Emails { + emails[i] = &models.EmailAddress{ + UID: c.User.ID, + Email: form.Emails[i], + IsActivated: !setting.Service.RegisterEmailConfirm, + } + } + + if err := models.AddEmailAddresses(emails); err != nil { + if models.IsErrEmailAlreadyUsed(err) { + c.Error(422, "", "Email address has been used: "+err.(models.ErrEmailAlreadyUsed).Email) + } else { + c.Error(500, "AddEmailAddresses", err) + } + return + } + + apiEmails := make([]*api.Email, len(emails)) + for i := range emails { + apiEmails[i] = convert.ToEmail(emails[i]) + } + c.JSON(201, &apiEmails) +} + +// https://github.com/gogits/go-gogs-client/wiki/Users-Emails#delete-email-addresses +func DeleteEmail(c *context.APIContext, form api.CreateEmailOption) { + if len(form.Emails) == 0 { + c.Status(204) + return + } + + emails := make([]*models.EmailAddress, len(form.Emails)) + for i := range form.Emails { + emails[i] = &models.EmailAddress{ + UID: c.User.ID, + Email: form.Emails[i], + } + } + + if err := models.DeleteEmailAddresses(emails); err != nil { + c.Error(500, "DeleteEmailAddresses", err) + return + } + c.Status(204) +} diff --git a/routes/api/v1/user/follower.go b/routes/api/v1/user/follower.go new file mode 100644 index 00000000..6bbd4c7e --- /dev/null +++ b/routes/api/v1/user/follower.go @@ -0,0 +1,120 @@ +// 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/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" +) + +func responseApiUsers(c *context.APIContext, users []*models.User) { + apiUsers := make([]*api.User, len(users)) + for i := range users { + apiUsers[i] = users[i].APIFormat() + } + c.JSON(200, &apiUsers) +} + +func listUserFollowers(c *context.APIContext, u *models.User) { + users, err := u.GetFollowers(c.QueryInt("page")) + if err != nil { + c.Error(500, "GetUserFollowers", err) + return + } + responseApiUsers(c, users) +} + +func ListMyFollowers(c *context.APIContext) { + listUserFollowers(c, c.User) +} + +// https://github.com/gogits/go-gogs-client/wiki/Users-Followers#list-followers-of-a-user +func ListFollowers(c *context.APIContext) { + u := GetUserByParams(c) + if c.Written() { + return + } + listUserFollowers(c, u) +} + +func listUserFollowing(c *context.APIContext, u *models.User) { + users, err := u.GetFollowing(c.QueryInt("page")) + if err != nil { + c.Error(500, "GetFollowing", err) + return + } + responseApiUsers(c, users) +} + +func ListMyFollowing(c *context.APIContext) { + listUserFollowing(c, c.User) +} + +// https://github.com/gogits/go-gogs-client/wiki/Users-Followers#list-users-followed-by-another-user +func ListFollowing(c *context.APIContext) { + u := GetUserByParams(c) + if c.Written() { + return + } + listUserFollowing(c, u) +} + +func checkUserFollowing(c *context.APIContext, u *models.User, followID int64) { + if u.IsFollowing(followID) { + c.Status(204) + } else { + c.Status(404) + } +} + +// https://github.com/gogits/go-gogs-client/wiki/Users-Followers#check-if-you-are-following-a-user +func CheckMyFollowing(c *context.APIContext) { + target := GetUserByParams(c) + if c.Written() { + return + } + checkUserFollowing(c, c.User, target.ID) +} + +// https://github.com/gogits/go-gogs-client/wiki/Users-Followers#check-if-one-user-follows-another +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) +} + +// https://github.com/gogits/go-gogs-client/wiki/Users-Followers#follow-a-user +func Follow(c *context.APIContext) { + target := GetUserByParams(c) + if c.Written() { + return + } + if err := models.FollowUser(c.User.ID, target.ID); err != nil { + c.Error(500, "FollowUser", err) + return + } + c.Status(204) +} + +// https://github.com/gogits/go-gogs-client/wiki/Users-Followers#unfollow-a-user +func Unfollow(c *context.APIContext) { + target := GetUserByParams(c) + if c.Written() { + return + } + if err := models.UnfollowUser(c.User.ID, target.ID); err != nil { + c.Error(500, "UnfollowUser", err) + return + } + c.Status(204) +} diff --git a/routes/api/v1/user/key.go b/routes/api/v1/user/key.go new file mode 100644 index 00000000..aef42afc --- /dev/null +++ b/routes/api/v1/user/key.go @@ -0,0 +1,120 @@ +// 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/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/routes/api/v1/convert" + "github.com/gogits/gogs/routes/api/v1/repo" +) + +func GetUserByParamsName(c *context.APIContext, name string) *models.User { + user, err := models.GetUserByName(c.Params(name)) + if err != nil { + if errors.IsUserNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetUserByName", err) + } + return nil + } + return user +} + +// GetUserByParams returns user whose name is presented in URL paramenter. +func GetUserByParams(c *context.APIContext) *models.User { + return GetUserByParamsName(c, ":username") +} + +func composePublicKeysAPILink() string { + return setting.AppURL + "api/v1/user/keys/" +} + +func listPublicKeys(c *context.APIContext, uid int64) { + keys, err := models.ListPublicKeys(uid) + if err != nil { + c.Error(500, "ListPublicKeys", err) + return + } + + apiLink := composePublicKeysAPILink() + apiKeys := make([]*api.PublicKey, len(keys)) + for i := range keys { + apiKeys[i] = convert.ToPublicKey(apiLink, keys[i]) + } + + c.JSON(200, &apiKeys) +} + +// https://github.com/gogits/go-gogs-client/wiki/Users-Public-Keys#list-your-public-keys +func ListMyPublicKeys(c *context.APIContext) { + listPublicKeys(c, c.User.ID) +} + +// https://github.com/gogits/go-gogs-client/wiki/Users-Public-Keys#list-public-keys-for-a-user +func ListPublicKeys(c *context.APIContext) { + user := GetUserByParams(c) + if c.Written() { + return + } + listPublicKeys(c, user.ID) +} + +// https://github.com/gogits/go-gogs-client/wiki/Users-Public-Keys#get-a-single-public-key +func GetPublicKey(c *context.APIContext) { + key, err := models.GetPublicKeyByID(c.ParamsInt64(":id")) + if err != nil { + if models.IsErrKeyNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetPublicKeyByID", err) + } + return + } + + apiLink := composePublicKeysAPILink() + c.JSON(200, convert.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 := models.CheckPublicKeyString(form.Key) + if err != nil { + repo.HandleCheckKeyStringError(c, err) + return + } + + key, err := models.AddPublicKey(uid, form.Title, content) + if err != nil { + repo.HandleAddKeyError(c, err) + return + } + apiLink := composePublicKeysAPILink() + c.JSON(201, convert.ToPublicKey(apiLink, key)) +} + +// https://github.com/gogits/go-gogs-client/wiki/Users-Public-Keys#create-a-public-key +func CreatePublicKey(c *context.APIContext, form api.CreateKeyOption) { + CreateUserPublicKey(c, form, c.User.ID) +} + +// https://github.com/gogits/go-gogs-client/wiki/Users-Public-Keys#delete-a-public-key +func DeletePublicKey(c *context.APIContext) { + if err := models.DeletePublicKey(c.User, c.ParamsInt64(":id")); err != nil { + if models.IsErrKeyAccessDenied(err) { + c.Error(403, "", "You do not have access to this key") + } else { + c.Error(500, "DeletePublicKey", err) + } + return + } + + c.Status(204) +} diff --git a/routes/api/v1/user/user.go b/routes/api/v1/user/user.go new file mode 100644 index 00000000..dbf727de --- /dev/null +++ b/routes/api/v1/user/user.go @@ -0,0 +1,75 @@ +// 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 ( + "github.com/Unknwon/com" + + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" +) + +func Search(c *context.APIContext) { + opts := &models.SearchUserOptions{ + Keyword: c.Query("q"), + Type: models.USER_TYPE_INDIVIDUAL, + PageSize: com.StrTo(c.Query("limit")).MustInt(), + } + if opts.PageSize == 0 { + opts.PageSize = 10 + } + + users, _, err := models.SearchUserByName(opts) + if err != nil { + c.JSON(500, 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: users[i].FullName, + } + if c.IsLogged { + results[i].Email = users[i].Email + } + } + + c.JSON(200, map[string]interface{}{ + "ok": true, + "data": results, + }) +} + +func GetInfo(c *context.APIContext) { + u, err := models.GetUserByName(c.Params(":username")) + if err != nil { + if errors.IsUserNotExist(err) { + c.Status(404) + } else { + c.Error(500, "GetUserByName", err) + } + return + } + + // Hide user e-mail when API caller isn't signed in. + if !c.IsLogged { + u.Email = "" + } + c.JSON(200, u.APIFormat()) +} + +func GetAuthenticatedUser(c *context.APIContext) { + c.JSON(200, c.User.APIFormat()) +} diff --git a/routes/dev/template.go b/routes/dev/template.go new file mode 100644 index 00000000..00afa5c4 --- /dev/null +++ b/routes/dev/template.go @@ -0,0 +1,24 @@ +// 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 dev + +import ( + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" +) + +func TemplatePreview(c *context.Context) { + c.Data["User"] = models.User{Name: "Unknown"} + c.Data["AppName"] = setting.AppName + c.Data["AppVer"] = setting.AppVer + c.Data["AppURL"] = setting.AppURL + c.Data["Code"] = "2014031910370000009fff6782aadb2162b4a997acb69d4400888e0b9274657374" + c.Data["ActiveCodeLives"] = setting.Service.ActiveCodeLives / 60 + c.Data["ResetPwdCodeLives"] = setting.Service.ResetPwdCodeLives / 60 + c.Data["CurDbValue"] = "" + + c.HTML(200, (c.Params("*"))) +} diff --git a/routes/home.go b/routes/home.go new file mode 100644 index 00000000..9c713391 --- /dev/null +++ b/routes/home.go @@ -0,0 +1,163 @@ +// 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 routes + +import ( + "github.com/Unknwon/paginater" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/routes/user" +) + +const ( + HOME = "home" + EXPLORE_REPOS = "explore/repos" + EXPLORE_USERS = "explore/users" + EXPLORE_ORGANIZATIONS = "explore/organizations" +) + +func Home(c *context.Context) { + if c.IsLogged { + if !c.User.IsActive && setting.Service.RegisterEmailConfirm { + c.Data["Title"] = c.Tr("auth.active_your_account") + c.HTML(200, user.ACTIVATE) + } else { + user.Dashboard(c) + } + return + } + + // Check auto-login. + uname := c.GetCookie(setting.CookieUserName) + if len(uname) != 0 { + c.Redirect(setting.AppSubURL + "/user/login") + return + } + + c.Data["PageIsHome"] = true + c.HTML(200, HOME) +} + +func ExploreRepos(c *context.Context) { + c.Data["Title"] = c.Tr("explore") + c.Data["PageIsExplore"] = true + c.Data["PageIsExploreRepositories"] = true + + page := c.QueryInt("page") + if page <= 0 { + page = 1 + } + + keyword := c.Query("q") + repos, count, err := models.SearchRepositoryByName(&models.SearchRepoOptions{ + Keyword: keyword, + UserID: c.UserID(), + OrderBy: "updated_unix DESC", + Page: page, + PageSize: setting.UI.ExplorePagingNum, + }) + if err != nil { + c.Handle(500, "SearchRepositoryByName", err) + return + } + c.Data["Keyword"] = keyword + c.Data["Total"] = count + c.Data["Page"] = paginater.New(int(count), setting.UI.ExplorePagingNum, page, 5) + + if err = models.RepositoryList(repos).LoadAttributes(); err != nil { + c.Handle(500, "LoadAttributes", err) + return + } + c.Data["Repos"] = repos + + c.HTML(200, EXPLORE_REPOS) +} + +type UserSearchOptions struct { + Type models.UserType + Counter func() int64 + Ranger func(int, int) ([]*models.User, error) + PageSize int + OrderBy string + TplName string +} + +func RenderUserSearch(c *context.Context, opts *UserSearchOptions) { + page := c.QueryInt("page") + if page <= 1 { + page = 1 + } + + var ( + users []*models.User + count int64 + err error + ) + + keyword := c.Query("q") + if len(keyword) == 0 { + users, err = opts.Ranger(page, opts.PageSize) + if err != nil { + c.Handle(500, "opts.Ranger", err) + return + } + count = opts.Counter() + } else { + users, count, err = models.SearchUserByName(&models.SearchUserOptions{ + Keyword: keyword, + Type: opts.Type, + OrderBy: opts.OrderBy, + Page: page, + PageSize: opts.PageSize, + }) + if err != nil { + c.Handle(500, "SearchUserByName", err) + return + } + } + c.Data["Keyword"] = keyword + c.Data["Total"] = count + c.Data["Page"] = paginater.New(int(count), opts.PageSize, page, 5) + c.Data["Users"] = users + + c.HTML(200, opts.TplName) +} + +func ExploreUsers(c *context.Context) { + c.Data["Title"] = c.Tr("explore") + c.Data["PageIsExplore"] = true + c.Data["PageIsExploreUsers"] = true + + RenderUserSearch(c, &UserSearchOptions{ + Type: models.USER_TYPE_INDIVIDUAL, + Counter: models.CountUsers, + Ranger: models.Users, + PageSize: setting.UI.ExplorePagingNum, + OrderBy: "updated_unix DESC", + TplName: EXPLORE_USERS, + }) +} + +func ExploreOrganizations(c *context.Context) { + c.Data["Title"] = c.Tr("explore") + c.Data["PageIsExplore"] = true + c.Data["PageIsExploreOrganizations"] = true + + RenderUserSearch(c, &UserSearchOptions{ + Type: models.USER_TYPE_ORGANIZATION, + Counter: models.CountOrganizations, + Ranger: models.Organizations, + PageSize: setting.UI.ExplorePagingNum, + OrderBy: "updated_unix DESC", + TplName: EXPLORE_ORGANIZATIONS, + }) +} + +func NotFound(c *context.Context) { + c.Data["Title"] = "Page Not Found" + c.NotFound() +} diff --git a/routes/install.go b/routes/install.go new file mode 100644 index 00000000..948c67c2 --- /dev/null +++ b/routes/install.go @@ -0,0 +1,392 @@ +// 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 routes + +import ( + "net/mail" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/Unknwon/com" + "github.com/go-xorm/xorm" + log "gopkg.in/clog.v1" + "gopkg.in/ini.v1" + "gopkg.in/macaron.v1" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/cron" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/mailer" + "github.com/gogits/gogs/pkg/markup" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/ssh" + "github.com/gogits/gogs/pkg/template/highlight" + "github.com/gogits/gogs/pkg/tool" + "github.com/gogits/gogs/pkg/user" +) + +const ( + INSTALL = "install" +) + +func checkRunMode() { + if setting.ProdMode { + macaron.Env = macaron.PROD + macaron.ColorLog = false + } else { + git.Debug = true + } + log.Info("Run Mode: %s", strings.Title(macaron.Env)) +} + +func NewServices() { + setting.NewServices() + mailer.NewContext() +} + +// GlobalInit is for global configuration reload-able. +func GlobalInit() { + setting.NewContext() + log.Trace("Custom path: %s", setting.CustomPath) + log.Trace("Log path: %s", setting.LogRootPath) + models.LoadConfigs() + NewServices() + + if setting.InstallLock { + highlight.NewContext() + markup.NewSanitizer() + if err := models.NewEngine(); err != nil { + log.Fatal(2, "Fail to initialize ORM engine: %v", err) + } + models.HasEngine = true + + models.LoadRepoConfig() + models.NewRepoContext() + + // Booting long running goroutines. + cron.NewContext() + models.InitSyncMirrors() + models.InitDeliverHooks() + models.InitTestPullRequests() + } + if models.EnableSQLite3 { + log.Info("SQLite3 Supported") + } + if setting.SupportMiniWinService { + log.Info("Builtin Windows Service Supported") + } + checkRunMode() + + if setting.InstallLock && setting.SSH.StartBuiltinServer { + ssh.Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers) + log.Info("SSH server started on %s:%v", setting.SSH.ListenHost, setting.SSH.ListenPort) + log.Trace("SSH server cipher list: %v", setting.SSH.ServerCiphers) + } +} + +func InstallInit(c *context.Context) { + if setting.InstallLock { + c.NotFound() + return + } + + c.Title("install.install") + c.PageIs("Install") + + dbOpts := []string{"MySQL", "PostgreSQL", "MSSQL"} + if models.EnableSQLite3 { + dbOpts = append(dbOpts, "SQLite3") + } + c.Data["DbOptions"] = dbOpts +} + +func Install(c *context.Context) { + f := form.Install{} + + // Database settings + f.DbHost = models.DbCfg.Host + f.DbUser = models.DbCfg.User + f.DbName = models.DbCfg.Name + f.DbPath = models.DbCfg.Path + + c.Data["CurDbOption"] = "MySQL" + switch models.DbCfg.Type { + case "postgres": + c.Data["CurDbOption"] = "PostgreSQL" + case "mssql": + c.Data["CurDbOption"] = "MSSQL" + case "sqlite3": + if models.EnableSQLite3 { + c.Data["CurDbOption"] = "SQLite3" + } + } + + // Application general settings + f.AppName = setting.AppName + f.RepoRootPath = setting.RepoRootPath + + // Note(unknwon): it's hard for Windows users change a running user, + // so just use current one if config says default. + if setting.IsWindows && setting.RunUser == "git" { + f.RunUser = user.CurrentUsername() + } else { + f.RunUser = setting.RunUser + } + + f.Domain = setting.Domain + f.SSHPort = setting.SSH.Port + f.UseBuiltinSSHServer = setting.SSH.StartBuiltinServer + f.HTTPPort = setting.HTTPPort + f.AppUrl = setting.AppURL + f.LogRootPath = setting.LogRootPath + + // E-mail service settings + if setting.MailService != nil { + f.SMTPHost = setting.MailService.Host + f.SMTPFrom = setting.MailService.From + f.SMTPUser = setting.MailService.User + } + f.RegisterConfirm = setting.Service.RegisterEmailConfirm + f.MailNotify = setting.Service.EnableNotifyMail + + // Server and other services settings + f.OfflineMode = setting.OfflineMode + f.DisableGravatar = setting.DisableGravatar + f.EnableFederatedAvatar = setting.EnableFederatedAvatar + f.DisableRegistration = setting.Service.DisableRegistration + f.EnableCaptcha = setting.Service.EnableCaptcha + f.RequireSignInView = setting.Service.RequireSignInView + + form.Assign(f, c.Data) + c.Success(INSTALL) +} + +func InstallPost(c *context.Context, f form.Install) { + c.Data["CurDbOption"] = f.DbType + + if c.HasError() { + if c.HasValue("Err_SMTPEmail") { + c.FormErr("SMTP") + } + if c.HasValue("Err_AdminName") || + c.HasValue("Err_AdminPasswd") || + c.HasValue("Err_AdminEmail") { + c.FormErr("Admin") + } + + c.Success(INSTALL) + return + } + + if _, err := exec.LookPath("git"); err != nil { + c.RenderWithErr(c.Tr("install.test_git_failed", err), INSTALL, &f) + return + } + + // Pass basic check, now test configuration. + // Test database setting. + dbTypes := map[string]string{"MySQL": "mysql", "PostgreSQL": "postgres", "MSSQL": "mssql", "SQLite3": "sqlite3", "TiDB": "tidb"} + models.DbCfg.Type = dbTypes[f.DbType] + models.DbCfg.Host = f.DbHost + models.DbCfg.User = f.DbUser + models.DbCfg.Passwd = f.DbPasswd + models.DbCfg.Name = f.DbName + models.DbCfg.SSLMode = f.SSLMode + models.DbCfg.Path = f.DbPath + + if models.DbCfg.Type == "sqlite3" && len(models.DbCfg.Path) == 0 { + c.FormErr("DbPath") + c.RenderWithErr(c.Tr("install.err_empty_db_path"), INSTALL, &f) + return + } + + // Set test engine. + var x *xorm.Engine + if err := models.NewTestEngine(x); err != nil { + if strings.Contains(err.Error(), `Unknown database type: sqlite3`) { + c.FormErr("DbType") + c.RenderWithErr(c.Tr("install.sqlite3_not_available", "https://gogs.io/docs/installation/install_from_binary.html"), INSTALL, &f) + } else { + c.FormErr("DbSetting") + c.RenderWithErr(c.Tr("install.invalid_db_setting", err), INSTALL, &f) + } + return + } + + // Test repository root path. + f.RepoRootPath = strings.Replace(f.RepoRootPath, "\\", "/", -1) + if err := os.MkdirAll(f.RepoRootPath, os.ModePerm); err != nil { + c.FormErr("RepoRootPath") + c.RenderWithErr(c.Tr("install.invalid_repo_path", err), INSTALL, &f) + return + } + + // Test log root path. + f.LogRootPath = strings.Replace(f.LogRootPath, "\\", "/", -1) + if err := os.MkdirAll(f.LogRootPath, os.ModePerm); err != nil { + c.FormErr("LogRootPath") + c.RenderWithErr(c.Tr("install.invalid_log_root_path", err), INSTALL, &f) + return + } + + currentUser, match := setting.IsRunUserMatchCurrentUser(f.RunUser) + if !match { + c.FormErr("RunUser") + c.RenderWithErr(c.Tr("install.run_user_not_match", f.RunUser, currentUser), INSTALL, &f) + return + } + + // Check host address and port + if len(f.SMTPHost) > 0 && !strings.Contains(f.SMTPHost, ":") { + c.FormErr("SMTP", "SMTPHost") + c.RenderWithErr(c.Tr("install.smtp_host_missing_port"), INSTALL, &f) + return + } + + // Make sure FROM field is valid + if len(f.SMTPFrom) > 0 { + _, err := mail.ParseAddress(f.SMTPFrom) + if err != nil { + c.FormErr("SMTP", "SMTPFrom") + c.RenderWithErr(c.Tr("install.invalid_smtp_from", err), INSTALL, &f) + return + } + } + + // Check logic loophole between disable self-registration and no admin account. + if f.DisableRegistration && len(f.AdminName) == 0 { + c.FormErr("Services", "Admin") + c.RenderWithErr(c.Tr("install.no_admin_and_disable_registration"), INSTALL, f) + return + } + + // Check admin password. + if len(f.AdminName) > 0 && len(f.AdminPasswd) == 0 { + c.FormErr("Admin", "AdminPasswd") + c.RenderWithErr(c.Tr("install.err_empty_admin_password"), INSTALL, f) + return + } + if f.AdminPasswd != f.AdminConfirmPasswd { + c.FormErr("Admin", "AdminPasswd") + c.RenderWithErr(c.Tr("form.password_not_match"), INSTALL, f) + return + } + + if f.AppUrl[len(f.AppUrl)-1] != '/' { + f.AppUrl += "/" + } + + // Save settings. + cfg := ini.Empty() + if com.IsFile(setting.CustomConf) { + // Keeps custom settings if there is already something. + if err := cfg.Append(setting.CustomConf); err != nil { + log.Error(2, "Fail to load custom conf '%s': %v", setting.CustomConf, err) + } + } + cfg.Section("database").Key("DB_TYPE").SetValue(models.DbCfg.Type) + cfg.Section("database").Key("HOST").SetValue(models.DbCfg.Host) + cfg.Section("database").Key("NAME").SetValue(models.DbCfg.Name) + cfg.Section("database").Key("USER").SetValue(models.DbCfg.User) + cfg.Section("database").Key("PASSWD").SetValue(models.DbCfg.Passwd) + cfg.Section("database").Key("SSL_MODE").SetValue(models.DbCfg.SSLMode) + cfg.Section("database").Key("PATH").SetValue(models.DbCfg.Path) + + cfg.Section("").Key("APP_NAME").SetValue(f.AppName) + cfg.Section("repository").Key("ROOT").SetValue(f.RepoRootPath) + cfg.Section("").Key("RUN_USER").SetValue(f.RunUser) + cfg.Section("server").Key("DOMAIN").SetValue(f.Domain) + cfg.Section("server").Key("HTTP_PORT").SetValue(f.HTTPPort) + cfg.Section("server").Key("ROOT_URL").SetValue(f.AppUrl) + + if f.SSHPort == 0 { + cfg.Section("server").Key("DISABLE_SSH").SetValue("true") + } else { + cfg.Section("server").Key("DISABLE_SSH").SetValue("false") + cfg.Section("server").Key("SSH_PORT").SetValue(com.ToStr(f.SSHPort)) + cfg.Section("server").Key("START_SSH_SERVER").SetValue(com.ToStr(f.UseBuiltinSSHServer)) + } + + if len(strings.TrimSpace(f.SMTPHost)) > 0 { + cfg.Section("mailer").Key("ENABLED").SetValue("true") + cfg.Section("mailer").Key("HOST").SetValue(f.SMTPHost) + cfg.Section("mailer").Key("FROM").SetValue(f.SMTPFrom) + cfg.Section("mailer").Key("USER").SetValue(f.SMTPUser) + cfg.Section("mailer").Key("PASSWD").SetValue(f.SMTPPasswd) + } else { + cfg.Section("mailer").Key("ENABLED").SetValue("false") + } + cfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").SetValue(com.ToStr(f.RegisterConfirm)) + cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(com.ToStr(f.MailNotify)) + + cfg.Section("server").Key("OFFLINE_MODE").SetValue(com.ToStr(f.OfflineMode)) + cfg.Section("picture").Key("DISABLE_GRAVATAR").SetValue(com.ToStr(f.DisableGravatar)) + cfg.Section("picture").Key("ENABLE_FEDERATED_AVATAR").SetValue(com.ToStr(f.EnableFederatedAvatar)) + cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(com.ToStr(f.DisableRegistration)) + cfg.Section("service").Key("ENABLE_CAPTCHA").SetValue(com.ToStr(f.EnableCaptcha)) + cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(com.ToStr(f.RequireSignInView)) + + cfg.Section("").Key("RUN_MODE").SetValue("prod") + + cfg.Section("session").Key("PROVIDER").SetValue("file") + + mode := "file" + if f.EnableConsoleMode { + mode = "console, file" + } + cfg.Section("log").Key("MODE").SetValue(mode) + cfg.Section("log").Key("LEVEL").SetValue("Info") + cfg.Section("log").Key("ROOT_PATH").SetValue(f.LogRootPath) + + cfg.Section("security").Key("INSTALL_LOCK").SetValue("true") + secretKey, err := tool.RandomString(15) + if err != nil { + c.RenderWithErr(c.Tr("install.secret_key_failed", err), INSTALL, &f) + return + } + cfg.Section("security").Key("SECRET_KEY").SetValue(secretKey) + + os.MkdirAll(filepath.Dir(setting.CustomConf), os.ModePerm) + if err := cfg.SaveTo(setting.CustomConf); err != nil { + c.RenderWithErr(c.Tr("install.save_config_failed", err), INSTALL, &f) + return + } + + GlobalInit() + + // Create admin account + if len(f.AdminName) > 0 { + u := &models.User{ + Name: f.AdminName, + Email: f.AdminEmail, + Passwd: f.AdminPasswd, + IsAdmin: true, + IsActive: true, + } + if err := models.CreateUser(u); err != nil { + if !models.IsErrUserAlreadyExist(err) { + setting.InstallLock = false + c.FormErr("AdminName", "AdminEmail") + c.RenderWithErr(c.Tr("install.invalid_admin_setting", err), INSTALL, &f) + return + } + log.Info("Admin account already exist") + u, _ = models.GetUserByName(u.Name) + } + + // Auto-login for admin + c.Session.Set("uid", u.ID) + c.Session.Set("uname", u.Name) + } + + log.Info("First-time run install finished!") + c.Flash.Success(c.Tr("install.install_success")) + c.Redirect(f.AppUrl + "user/login") +} diff --git a/routes/org/members.go b/routes/org/members.go new file mode 100644 index 00000000..b529748c --- /dev/null +++ b/routes/org/members.go @@ -0,0 +1,123 @@ +// 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 org + +import ( + "github.com/Unknwon/com" + log "gopkg.in/clog.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" +) + +const ( + MEMBERS = "org/member/members" + MEMBER_INVITE = "org/member/invite" +) + +func Members(c *context.Context) { + org := c.Org.Organization + c.Data["Title"] = org.FullName + c.Data["PageIsOrgMembers"] = true + + if err := org.GetMembers(); err != nil { + c.Handle(500, "GetMembers", err) + return + } + c.Data["Members"] = org.Members + + c.HTML(200, MEMBERS) +} + +func MembersAction(c *context.Context) { + uid := com.StrTo(c.Query("uid")).MustInt64() + if uid == 0 { + c.Redirect(c.Org.OrgLink + "/members") + return + } + + org := c.Org.Organization + var err error + switch c.Params(":action") { + case "private": + if c.User.ID != uid && !c.Org.IsOwner { + c.Error(404) + return + } + err = models.ChangeOrgUserStatus(org.ID, uid, false) + case "public": + if c.User.ID != uid && !c.Org.IsOwner { + c.Error(404) + return + } + err = models.ChangeOrgUserStatus(org.ID, uid, true) + case "remove": + if !c.Org.IsOwner { + c.Error(404) + return + } + err = org.RemoveMember(uid) + if models.IsErrLastOrgOwner(err) { + c.Flash.Error(c.Tr("form.last_org_owner")) + c.Redirect(c.Org.OrgLink + "/members") + return + } + case "leave": + err = org.RemoveMember(c.User.ID) + if models.IsErrLastOrgOwner(err) { + c.Flash.Error(c.Tr("form.last_org_owner")) + c.Redirect(c.Org.OrgLink + "/members") + return + } + } + + if err != nil { + log.Error(4, "Action(%s): %v", c.Params(":action"), err) + c.JSON(200, map[string]interface{}{ + "ok": false, + "err": err.Error(), + }) + return + } + + if c.Params(":action") != "leave" { + c.Redirect(c.Org.OrgLink + "/members") + } else { + c.Redirect(setting.AppSubURL + "/") + } +} + +func Invitation(c *context.Context) { + org := c.Org.Organization + c.Data["Title"] = org.FullName + c.Data["PageIsOrgMembers"] = true + + if c.Req.Method == "POST" { + uname := c.Query("uname") + u, err := models.GetUserByName(uname) + if err != nil { + if errors.IsUserNotExist(err) { + c.Flash.Error(c.Tr("form.user_not_exist")) + c.Redirect(c.Org.OrgLink + "/invitations/new") + } else { + c.Handle(500, " GetUserByName", err) + } + return + } + + if err = org.AddMember(u.ID); err != nil { + c.Handle(500, " AddMember", err) + return + } + + log.Trace("New member added(%s): %s", org.Name, u.Name) + c.Redirect(c.Org.OrgLink + "/members") + return + } + + c.HTML(200, MEMBER_INVITE) +} diff --git a/routes/org/org.go b/routes/org/org.go new file mode 100644 index 00000000..775e9915 --- /dev/null +++ b/routes/org/org.go @@ -0,0 +1,56 @@ +// 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 org + +import ( + log "gopkg.in/clog.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/setting" +) + +const ( + CREATE = "org/create" +) + +func Create(c *context.Context) { + c.Data["Title"] = c.Tr("new_org") + c.HTML(200, CREATE) +} + +func CreatePost(c *context.Context, f form.CreateOrg) { + c.Data["Title"] = c.Tr("new_org") + + if c.HasError() { + c.HTML(200, CREATE) + return + } + + org := &models.User{ + Name: f.OrgName, + IsActive: true, + Type: models.USER_TYPE_ORGANIZATION, + } + + if err := models.CreateOrganization(org, c.User); err != nil { + c.Data["Err_OrgName"] = true + switch { + case models.IsErrUserAlreadyExist(err): + c.RenderWithErr(c.Tr("form.org_name_been_taken"), CREATE, &f) + case models.IsErrNameReserved(err): + c.RenderWithErr(c.Tr("org.form.name_reserved", err.(models.ErrNameReserved).Name), CREATE, &f) + case models.IsErrNamePatternNotAllowed(err): + c.RenderWithErr(c.Tr("org.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), CREATE, &f) + default: + c.Handle(500, "CreateOrganization", err) + } + return + } + log.Trace("Organization created: %s", org.Name) + + c.Redirect(setting.AppSubURL + "/org/" + f.OrgName + "/dashboard") +} diff --git a/routes/org/setting.go b/routes/org/setting.go new file mode 100644 index 00000000..397ffa8f --- /dev/null +++ b/routes/org/setting.go @@ -0,0 +1,168 @@ +// 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 org + +import ( + "strings" + + log "gopkg.in/clog.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/routes/user" +) + +const ( + SETTINGS_OPTIONS = "org/settings/options" + SETTINGS_DELETE = "org/settings/delete" + SETTINGS_WEBHOOKS = "org/settings/webhooks" +) + +func Settings(c *context.Context) { + c.Data["Title"] = c.Tr("org.settings") + c.Data["PageIsSettingsOptions"] = true + c.HTML(200, SETTINGS_OPTIONS) +} + +func SettingsPost(c *context.Context, f form.UpdateOrgSetting) { + c.Data["Title"] = c.Tr("org.settings") + c.Data["PageIsSettingsOptions"] = true + + if c.HasError() { + c.HTML(200, SETTINGS_OPTIONS) + return + } + + org := c.Org.Organization + + // Check if organization name has been changed. + if org.LowerName != strings.ToLower(f.Name) { + isExist, err := models.IsUserExist(org.ID, f.Name) + if err != nil { + c.Handle(500, "IsUserExist", err) + return + } else if isExist { + c.Data["OrgName"] = true + c.RenderWithErr(c.Tr("form.username_been_taken"), SETTINGS_OPTIONS, &f) + return + } else if err = models.ChangeUserName(org, f.Name); err != nil { + c.Data["OrgName"] = true + switch { + case models.IsErrNameReserved(err): + c.RenderWithErr(c.Tr("user.form.name_reserved"), SETTINGS_OPTIONS, &f) + case models.IsErrNamePatternNotAllowed(err): + c.RenderWithErr(c.Tr("user.form.name_pattern_not_allowed"), SETTINGS_OPTIONS, &f) + default: + c.Handle(500, "ChangeUserName", err) + } + return + } + // reset c.org.OrgLink with new name + c.Org.OrgLink = setting.AppSubURL + "/org/" + f.Name + log.Trace("Organization name changed: %s -> %s", org.Name, f.Name) + } + // In case it's just a case change. + org.Name = f.Name + org.LowerName = strings.ToLower(f.Name) + + if c.User.IsAdmin { + org.MaxRepoCreation = f.MaxRepoCreation + } + + org.FullName = f.FullName + org.Description = f.Description + org.Website = f.Website + org.Location = f.Location + if err := models.UpdateUser(org); err != nil { + c.Handle(500, "UpdateUser", err) + return + } + log.Trace("Organization setting updated: %s", org.Name) + c.Flash.Success(c.Tr("org.settings.update_setting_success")) + c.Redirect(c.Org.OrgLink + "/settings") +} + +func SettingsAvatar(c *context.Context, f form.Avatar) { + f.Source = form.AVATAR_LOCAL + if err := user.UpdateAvatarSetting(c, f, c.Org.Organization); err != nil { + c.Flash.Error(err.Error()) + } else { + c.Flash.Success(c.Tr("org.settings.update_avatar_success")) + } + + c.Redirect(c.Org.OrgLink + "/settings") +} + +func SettingsDeleteAvatar(c *context.Context) { + if err := c.Org.Organization.DeleteAvatar(); err != nil { + c.Flash.Error(err.Error()) + } + + c.Redirect(c.Org.OrgLink + "/settings") +} + +func SettingsDelete(c *context.Context) { + c.Data["Title"] = c.Tr("org.settings") + c.Data["PageIsSettingsDelete"] = true + + org := c.Org.Organization + if c.Req.Method == "POST" { + if _, err := models.UserSignIn(c.User.Name, c.Query("password")); err != nil { + if errors.IsUserNotExist(err) { + c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SETTINGS_DELETE, nil) + } else { + c.Handle(500, "UserSignIn", err) + } + return + } + + if err := models.DeleteOrganization(org); err != nil { + if models.IsErrUserOwnRepos(err) { + c.Flash.Error(c.Tr("form.org_still_own_repo")) + c.Redirect(c.Org.OrgLink + "/settings/delete") + } else { + c.Handle(500, "DeleteOrganization", err) + } + } else { + log.Trace("Organization deleted: %s", org.Name) + c.Redirect(setting.AppSubURL + "/") + } + return + } + + c.HTML(200, SETTINGS_DELETE) +} + +func Webhooks(c *context.Context) { + c.Data["Title"] = c.Tr("org.settings") + c.Data["PageIsSettingsHooks"] = true + c.Data["BaseLink"] = c.Org.OrgLink + c.Data["Description"] = c.Tr("org.settings.hooks_desc") + c.Data["Types"] = setting.Webhook.Types + + ws, err := models.GetWebhooksByOrgID(c.Org.Organization.ID) + if err != nil { + c.Handle(500, "GetWebhooksByOrgId", err) + return + } + + c.Data["Webhooks"] = ws + c.HTML(200, SETTINGS_WEBHOOKS) +} + +func DeleteWebhook(c *context.Context) { + if err := models.DeleteWebhookOfOrgByID(c.Org.Organization.ID, c.QueryInt64("id")); err != nil { + c.Flash.Error("DeleteWebhookByOrgID: " + err.Error()) + } else { + c.Flash.Success(c.Tr("repo.settings.webhook_deletion_success")) + } + + c.JSON(200, map[string]interface{}{ + "redirect": c.Org.OrgLink + "/settings/hooks", + }) +} diff --git a/routes/org/teams.go b/routes/org/teams.go new file mode 100644 index 00000000..c97d470d --- /dev/null +++ b/routes/org/teams.go @@ -0,0 +1,271 @@ +// 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 org + +import ( + "path" + + "github.com/Unknwon/com" + log "gopkg.in/clog.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" +) + +const ( + TEAMS = "org/team/teams" + TEAM_NEW = "org/team/new" + TEAM_MEMBERS = "org/team/members" + TEAM_REPOSITORIES = "org/team/repositories" +) + +func Teams(c *context.Context) { + org := c.Org.Organization + c.Data["Title"] = org.FullName + c.Data["PageIsOrgTeams"] = true + + for _, t := range org.Teams { + if err := t.GetMembers(); err != nil { + c.Handle(500, "GetMembers", err) + return + } + } + c.Data["Teams"] = org.Teams + + c.HTML(200, TEAMS) +} + +func TeamsAction(c *context.Context) { + uid := com.StrTo(c.Query("uid")).MustInt64() + if uid == 0 { + c.Redirect(c.Org.OrgLink + "/teams") + return + } + + page := c.Query("page") + var err error + switch c.Params(":action") { + case "join": + if !c.Org.IsOwner { + c.Error(404) + return + } + err = c.Org.Team.AddMember(c.User.ID) + case "leave": + err = c.Org.Team.RemoveMember(c.User.ID) + case "remove": + if !c.Org.IsOwner { + c.Error(404) + return + } + err = c.Org.Team.RemoveMember(uid) + page = "team" + case "add": + if !c.Org.IsOwner { + c.Error(404) + return + } + uname := c.Query("uname") + var u *models.User + u, err = models.GetUserByName(uname) + if err != nil { + if errors.IsUserNotExist(err) { + c.Flash.Error(c.Tr("form.user_not_exist")) + c.Redirect(c.Org.OrgLink + "/teams/" + c.Org.Team.LowerName) + } else { + c.Handle(500, " GetUserByName", err) + } + return + } + + err = c.Org.Team.AddMember(u.ID) + page = "team" + } + + if err != nil { + if models.IsErrLastOrgOwner(err) { + c.Flash.Error(c.Tr("form.last_org_owner")) + } else { + log.Error(3, "Action(%s): %v", c.Params(":action"), err) + c.JSON(200, map[string]interface{}{ + "ok": false, + "err": err.Error(), + }) + return + } + } + + switch page { + case "team": + c.Redirect(c.Org.OrgLink + "/teams/" + c.Org.Team.LowerName) + default: + c.Redirect(c.Org.OrgLink + "/teams") + } +} + +func TeamsRepoAction(c *context.Context) { + if !c.Org.IsOwner { + c.Error(404) + return + } + + var err error + switch c.Params(":action") { + case "add": + repoName := path.Base(c.Query("repo_name")) + var repo *models.Repository + repo, err = models.GetRepositoryByName(c.Org.Organization.ID, repoName) + if err != nil { + if errors.IsRepoNotExist(err) { + c.Flash.Error(c.Tr("org.teams.add_nonexistent_repo")) + c.Redirect(c.Org.OrgLink + "/teams/" + c.Org.Team.LowerName + "/repositories") + return + } + c.Handle(500, "GetRepositoryByName", err) + return + } + err = c.Org.Team.AddRepository(repo) + case "remove": + err = c.Org.Team.RemoveRepository(com.StrTo(c.Query("repoid")).MustInt64()) + } + + if err != nil { + log.Error(3, "Action(%s): '%s' %v", c.Params(":action"), c.Org.Team.Name, err) + c.Handle(500, "TeamsRepoAction", err) + return + } + c.Redirect(c.Org.OrgLink + "/teams/" + c.Org.Team.LowerName + "/repositories") +} + +func NewTeam(c *context.Context) { + c.Data["Title"] = c.Org.Organization.FullName + c.Data["PageIsOrgTeams"] = true + c.Data["PageIsOrgTeamsNew"] = true + c.Data["Team"] = &models.Team{} + c.HTML(200, TEAM_NEW) +} + +func NewTeamPost(c *context.Context, f form.CreateTeam) { + c.Data["Title"] = c.Org.Organization.FullName + c.Data["PageIsOrgTeams"] = true + c.Data["PageIsOrgTeamsNew"] = true + + t := &models.Team{ + OrgID: c.Org.Organization.ID, + Name: f.TeamName, + Description: f.Description, + Authorize: models.ParseAccessMode(f.Permission), + } + c.Data["Team"] = t + + if c.HasError() { + c.HTML(200, TEAM_NEW) + return + } + + if err := models.NewTeam(t); err != nil { + c.Data["Err_TeamName"] = true + switch { + case models.IsErrTeamAlreadyExist(err): + c.RenderWithErr(c.Tr("form.team_name_been_taken"), TEAM_NEW, &f) + case models.IsErrNameReserved(err): + c.RenderWithErr(c.Tr("org.form.team_name_reserved", err.(models.ErrNameReserved).Name), TEAM_NEW, &f) + default: + c.Handle(500, "NewTeam", err) + } + return + } + log.Trace("Team created: %s/%s", c.Org.Organization.Name, t.Name) + c.Redirect(c.Org.OrgLink + "/teams/" + t.LowerName) +} + +func TeamMembers(c *context.Context) { + c.Data["Title"] = c.Org.Team.Name + c.Data["PageIsOrgTeams"] = true + if err := c.Org.Team.GetMembers(); err != nil { + c.Handle(500, "GetMembers", err) + return + } + c.HTML(200, TEAM_MEMBERS) +} + +func TeamRepositories(c *context.Context) { + c.Data["Title"] = c.Org.Team.Name + c.Data["PageIsOrgTeams"] = true + if err := c.Org.Team.GetRepositories(); err != nil { + c.Handle(500, "GetRepositories", err) + return + } + c.HTML(200, TEAM_REPOSITORIES) +} + +func EditTeam(c *context.Context) { + c.Data["Title"] = c.Org.Organization.FullName + c.Data["PageIsOrgTeams"] = true + c.Data["team_name"] = c.Org.Team.Name + c.Data["desc"] = c.Org.Team.Description + c.HTML(200, TEAM_NEW) +} + +func EditTeamPost(c *context.Context, f form.CreateTeam) { + t := c.Org.Team + c.Data["Title"] = c.Org.Organization.FullName + c.Data["PageIsOrgTeams"] = true + c.Data["Team"] = t + + if c.HasError() { + c.HTML(200, TEAM_NEW) + return + } + + isAuthChanged := false + if !t.IsOwnerTeam() { + // Validate permission level. + var auth models.AccessMode + switch f.Permission { + case "read": + auth = models.ACCESS_MODE_READ + case "write": + auth = models.ACCESS_MODE_WRITE + case "admin": + auth = models.ACCESS_MODE_ADMIN + default: + c.Error(401) + return + } + + t.Name = f.TeamName + if t.Authorize != auth { + isAuthChanged = true + t.Authorize = auth + } + } + t.Description = f.Description + if err := models.UpdateTeam(t, isAuthChanged); err != nil { + c.Data["Err_TeamName"] = true + switch { + case models.IsErrTeamAlreadyExist(err): + c.RenderWithErr(c.Tr("form.team_name_been_taken"), TEAM_NEW, &f) + default: + c.Handle(500, "UpdateTeam", err) + } + return + } + c.Redirect(c.Org.OrgLink + "/teams/" + t.LowerName) +} + +func DeleteTeam(c *context.Context) { + if err := models.DeleteTeam(c.Org.Team); err != nil { + c.Flash.Error("DeleteTeam: " + err.Error()) + } else { + c.Flash.Success(c.Tr("org.teams.delete_team_success")) + } + + c.JSON(200, map[string]interface{}{ + "redirect": c.Org.OrgLink + "/teams", + }) +} diff --git a/routes/repo/branch.go b/routes/repo/branch.go new file mode 100644 index 00000000..685df2e7 --- /dev/null +++ b/routes/repo/branch.go @@ -0,0 +1,142 @@ +// 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 ( + "time" + + log "gopkg.in/clog.v1" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" +) + +const ( + BRANCHES_OVERVIEW = "repo/branches/overview" + BRANCHES_ALL = "repo/branches/all" +) + +type Branch struct { + Name string + Commit *git.Commit + IsProtected bool +} + +func loadBranches(c *context.Context) []*Branch { + rawBranches, err := c.Repo.Repository.GetBranches() + if err != nil { + c.Handle(500, "GetBranches", err) + return nil + } + + protectBranches, err := models.GetProtectBranchesByRepoID(c.Repo.Repository.ID) + if err != nil { + c.Handle(500, "GetProtectBranchesByRepoID", err) + return nil + } + + branches := make([]*Branch, len(rawBranches)) + for i := range rawBranches { + commit, err := rawBranches[i].GetCommit() + if err != nil { + c.Handle(500, "GetCommit", err) + return nil + } + + branches[i] = &Branch{ + Name: rawBranches[i].Name, + Commit: commit, + } + + for j := range protectBranches { + if branches[i].Name == protectBranches[j].Name { + branches[i].IsProtected = true + break + } + } + } + + c.Data["AllowPullRequest"] = c.Repo.Repository.AllowsPulls() + return branches +} + +func Branches(c *context.Context) { + c.Data["Title"] = c.Tr("repo.git_branches") + c.Data["PageIsBranchesOverview"] = true + + branches := loadBranches(c) + if c.Written() { + return + } + + now := time.Now() + activeBranches := make([]*Branch, 0, 3) + staleBranches := make([]*Branch, 0, 3) + for i := range branches { + switch { + case branches[i].Name == c.Repo.BranchName: + c.Data["DefaultBranch"] = branches[i] + case branches[i].Commit.Committer.When.Add(30 * 24 * time.Hour).After(now): // 30 days + activeBranches = append(activeBranches, branches[i]) + case branches[i].Commit.Committer.When.Add(3 * 30 * 24 * time.Hour).Before(now): // 90 days + staleBranches = append(staleBranches, branches[i]) + } + } + + c.Data["ActiveBranches"] = activeBranches + c.Data["StaleBranches"] = staleBranches + c.HTML(200, BRANCHES_OVERVIEW) +} + +func AllBranches(c *context.Context) { + c.Data["Title"] = c.Tr("repo.git_branches") + c.Data["PageIsBranchesAll"] = true + + branches := loadBranches(c) + if c.Written() { + return + } + c.Data["Branches"] = branches + + c.HTML(200, BRANCHES_ALL) +} + +func DeleteBranchPost(c *context.Context) { + branchName := c.Params("*") + commitID := c.Query("commit") + + defer func() { + redirectTo := c.Query("redirect_to") + if len(redirectTo) == 0 { + redirectTo = c.Repo.RepoLink + } + c.Redirect(redirectTo) + }() + + if !c.Repo.GitRepo.IsBranchExist(branchName) { + return + } + if len(commitID) > 0 { + branchCommitID, err := c.Repo.GitRepo.GetBranchCommitID(branchName) + if err != nil { + log.Error(2, "GetBranchCommitID: %v", err) + return + } + + if branchCommitID != commitID { + c.Flash.Error(c.Tr("repo.pulls.delete_branch_has_new_commits")) + return + } + } + + if err := c.Repo.GitRepo.DeleteBranch(branchName, git.DeleteBranchOptions{ + Force: true, + }); err != nil { + log.Error(2, "DeleteBranch '%s': %v", branchName, err) + return + } +} diff --git a/routes/repo/commit.go b/routes/repo/commit.go new file mode 100644 index 00000000..17ea5dbe --- /dev/null +++ b/routes/repo/commit.go @@ -0,0 +1,239 @@ +// 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 ( + "container/list" + "path" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/tool" +) + +const ( + COMMITS = "repo/commits" + DIFF = "repo/diff/page" +) + +func RefCommits(c *context.Context) { + c.Data["PageIsViewFiles"] = true + switch { + case len(c.Repo.TreePath) == 0: + Commits(c) + case c.Repo.TreePath == "search": + SearchCommits(c) + default: + FileHistory(c) + } +} + +func RenderIssueLinks(oldCommits *list.List, repoLink string) *list.List { + newCommits := list.New() + for e := oldCommits.Front(); e != nil; e = e.Next() { + c := e.Value.(*git.Commit) + newCommits.PushBack(c) + } + return newCommits +} + +func renderCommits(c *context.Context, filename string) { + c.Data["Title"] = c.Tr("repo.commits.commit_history") + " · " + c.Repo.Repository.FullName() + c.Data["PageIsCommits"] = true + + page := c.QueryInt("page") + if page < 1 { + page = 1 + } + pageSize := c.QueryInt("pageSize") + if pageSize < 1 { + pageSize = git.DefaultCommitsPageSize + } + + // Both 'git log branchName' and 'git log commitID' work. + var err error + var commits *list.List + if len(filename) == 0 { + commits, err = c.Repo.Commit.CommitsByRangeSize(page, pageSize) + } else { + commits, err = c.Repo.GitRepo.CommitsByFileAndRangeSize(c.Repo.BranchName, filename, page, pageSize) + } + if err != nil { + c.Handle(500, "CommitsByRangeSize/CommitsByFileAndRangeSize", err) + return + } + commits = RenderIssueLinks(commits, c.Repo.RepoLink) + commits = models.ValidateCommitsWithEmails(commits) + c.Data["Commits"] = commits + + if page > 1 { + c.Data["HasPrevious"] = true + c.Data["PreviousPage"] = page - 1 + } + if commits.Len() == pageSize { + c.Data["HasNext"] = true + c.Data["NextPage"] = page + 1 + } + c.Data["PageSize"] = pageSize + + c.Data["Username"] = c.Repo.Owner.Name + c.Data["Reponame"] = c.Repo.Repository.Name + c.HTML(200, COMMITS) +} + +func Commits(c *context.Context) { + renderCommits(c, "") +} + +func SearchCommits(c *context.Context) { + c.Data["PageIsCommits"] = true + + keyword := c.Query("q") + if len(keyword) == 0 { + c.Redirect(c.Repo.RepoLink + "/commits/" + c.Repo.BranchName) + return + } + + commits, err := c.Repo.Commit.SearchCommits(keyword) + if err != nil { + c.Handle(500, "SearchCommits", err) + return + } + commits = RenderIssueLinks(commits, c.Repo.RepoLink) + commits = models.ValidateCommitsWithEmails(commits) + c.Data["Commits"] = commits + + c.Data["Keyword"] = keyword + c.Data["Username"] = c.Repo.Owner.Name + c.Data["Reponame"] = c.Repo.Repository.Name + c.Data["Branch"] = c.Repo.BranchName + c.HTML(200, COMMITS) +} + +func FileHistory(c *context.Context) { + renderCommits(c, c.Repo.TreePath) +} + +func Diff(c *context.Context) { + c.Data["PageIsDiff"] = true + c.Data["RequireHighlightJS"] = true + + userName := c.Repo.Owner.Name + repoName := c.Repo.Repository.Name + commitID := c.Params(":sha") + + commit, err := c.Repo.GitRepo.GetCommit(commitID) + if err != nil { + if git.IsErrNotExist(err) { + c.Handle(404, "Repo.GitRepo.GetCommit", err) + } else { + c.Handle(500, "Repo.GitRepo.GetCommit", err) + } + return + } + + diff, err := models.GetDiffCommit(models.RepoPath(userName, repoName), + commitID, setting.Git.MaxGitDiffLines, + setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles) + if err != nil { + c.NotFoundOrServerError("GetDiffCommit", git.IsErrNotExist, err) + return + } + + parents := make([]string, commit.ParentCount()) + for i := 0; i < commit.ParentCount(); i++ { + sha, err := commit.ParentID(i) + parents[i] = sha.String() + if err != nil { + c.Handle(404, "repo.Diff", err) + return + } + } + + setEditorconfigIfExists(c) + if c.Written() { + return + } + + c.Data["CommitID"] = commitID + c.Data["IsSplitStyle"] = c.Query("style") == "split" + c.Data["Username"] = userName + c.Data["Reponame"] = repoName + c.Data["IsImageFile"] = commit.IsImageFile + c.Data["Title"] = commit.Summary() + " · " + tool.ShortSHA1(commitID) + c.Data["Commit"] = commit + c.Data["Author"] = models.ValidateCommitWithEmail(commit) + c.Data["Diff"] = diff + c.Data["Parents"] = parents + c.Data["DiffNotAvailable"] = diff.NumFiles() == 0 + c.Data["SourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", commitID) + if commit.ParentCount() > 0 { + c.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", parents[0]) + } + c.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "raw", commitID) + c.HTML(200, DIFF) +} + +func RawDiff(c *context.Context) { + if err := git.GetRawDiff( + models.RepoPath(c.Repo.Owner.Name, c.Repo.Repository.Name), + c.Params(":sha"), + git.RawDiffType(c.Params(":ext")), + c.Resp, + ); err != nil { + c.NotFoundOrServerError("GetRawDiff", git.IsErrNotExist, err) + return + } +} + +func CompareDiff(c *context.Context) { + c.Data["IsDiffCompare"] = true + userName := c.Repo.Owner.Name + repoName := c.Repo.Repository.Name + beforeCommitID := c.Params(":before") + afterCommitID := c.Params(":after") + + commit, err := c.Repo.GitRepo.GetCommit(afterCommitID) + if err != nil { + c.Handle(404, "GetCommit", err) + return + } + + diff, err := models.GetDiffRange(models.RepoPath(userName, repoName), beforeCommitID, + afterCommitID, setting.Git.MaxGitDiffLines, + setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles) + if err != nil { + c.Handle(404, "GetDiffRange", err) + return + } + + commits, err := commit.CommitsBeforeUntil(beforeCommitID) + if err != nil { + c.Handle(500, "CommitsBeforeUntil", err) + return + } + commits = models.ValidateCommitsWithEmails(commits) + + c.Data["IsSplitStyle"] = c.Query("style") == "split" + c.Data["CommitRepoLink"] = c.Repo.RepoLink + c.Data["Commits"] = commits + c.Data["CommitsCount"] = commits.Len() + c.Data["BeforeCommitID"] = beforeCommitID + c.Data["AfterCommitID"] = afterCommitID + c.Data["Username"] = userName + c.Data["Reponame"] = repoName + c.Data["IsImageFile"] = commit.IsImageFile + c.Data["Title"] = "Comparing " + tool.ShortSHA1(beforeCommitID) + "..." + tool.ShortSHA1(afterCommitID) + " · " + userName + "/" + repoName + c.Data["Commit"] = commit + c.Data["Diff"] = diff + c.Data["DiffNotAvailable"] = diff.NumFiles() == 0 + c.Data["SourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", afterCommitID) + c.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", beforeCommitID) + c.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "raw", afterCommitID) + c.HTML(200, DIFF) +} diff --git a/routes/repo/download.go b/routes/repo/download.go new file mode 100644 index 00000000..e9a29989 --- /dev/null +++ b/routes/repo/download.go @@ -0,0 +1,60 @@ +// 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 ( + "io" + "path" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/pkg/tool" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" +) + +func ServeData(c *context.Context, name string, reader io.Reader) error { + buf := make([]byte, 1024) + n, _ := reader.Read(buf) + if n >= 0 { + buf = buf[:n] + } + + if !tool.IsTextFile(buf) { + if !tool.IsImageFile(buf) { + c.Resp.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"") + c.Resp.Header().Set("Content-Transfer-Encoding", "binary") + } + } else if !setting.Repository.EnableRawFileRenderMode || !c.QueryBool("render") { + c.Resp.Header().Set("Content-Type", "text/plain; charset=utf-8") + } + c.Resp.Write(buf) + _, err := io.Copy(c.Resp, reader) + return err +} + +func ServeBlob(c *context.Context, blob *git.Blob) error { + dataRc, err := blob.Data() + if err != nil { + return err + } + + return ServeData(c, path.Base(c.Repo.TreePath), dataRc) +} + +func SingleDownload(c *context.Context) { + blob, err := c.Repo.Commit.GetBlobByPath(c.Repo.TreePath) + if err != nil { + if git.IsErrNotExist(err) { + c.Handle(404, "GetBlobByPath", nil) + } else { + c.Handle(500, "GetBlobByPath", err) + } + return + } + if err = ServeBlob(c, blob); err != nil { + c.Handle(500, "ServeBlob", err) + } +} diff --git a/routes/repo/editor.go b/routes/repo/editor.go new file mode 100644 index 00000000..4cd78d70 --- /dev/null +++ b/routes/repo/editor.go @@ -0,0 +1,571 @@ +// 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" + "io/ioutil" + "net/http" + "path" + "strings" + + log "gopkg.in/clog.v1" + + "github.com/gogits/git-module" + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/template" + "github.com/gogits/gogs/pkg/tool" +) + +const ( + EDIT_FILE = "repo/editor/edit" + EDIT_DIFF_PREVIEW = "repo/editor/diff_preview" + DELETE_FILE = "repo/editor/delete" + UPLOAD_FILE = "repo/editor/upload" +) + +// getParentTreeFields returns list of parent tree names and corresponding tree paths +// based on given tree path. +func getParentTreeFields(treePath string) (treeNames []string, treePaths []string) { + if len(treePath) == 0 { + return treeNames, treePaths + } + + treeNames = strings.Split(treePath, "/") + treePaths = make([]string, len(treeNames)) + for i := range treeNames { + treePaths[i] = strings.Join(treeNames[:i+1], "/") + } + return treeNames, treePaths +} + +func editFile(c *context.Context, isNewFile bool) { + c.PageIs("Edit") + c.RequireHighlightJS() + c.RequireSimpleMDE() + c.Data["IsNewFile"] = isNewFile + + treeNames, treePaths := getParentTreeFields(c.Repo.TreePath) + + if !isNewFile { + entry, err := c.Repo.Commit.GetTreeEntryByPath(c.Repo.TreePath) + if err != nil { + c.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err) + return + } + + // No way to edit a directory online. + if entry.IsDir() { + c.NotFound() + return + } + + blob := entry.Blob() + dataRc, err := blob.Data() + if err != nil { + c.ServerError("blob.Data", err) + return + } + + c.Data["FileSize"] = blob.Size() + c.Data["FileName"] = blob.Name() + + buf := make([]byte, 1024) + n, _ := dataRc.Read(buf) + buf = buf[:n] + + // Only text file are editable online. + if !tool.IsTextFile(buf) { + c.NotFound() + return + } + + d, _ := ioutil.ReadAll(dataRc) + buf = append(buf, d...) + if err, content := template.ToUTF8WithErr(buf); err != nil { + if err != nil { + log.Error(2, "ToUTF8WithErr: %v", err) + } + c.Data["FileContent"] = string(buf) + } else { + c.Data["FileContent"] = content + } + } else { + treeNames = append(treeNames, "") // Append empty string to allow user name the new file. + } + + c.Data["ParentTreePath"] = path.Dir(c.Repo.TreePath) + c.Data["TreeNames"] = treeNames + c.Data["TreePaths"] = treePaths + c.Data["BranchLink"] = c.Repo.RepoLink + "/src/" + c.Repo.BranchName + c.Data["commit_summary"] = "" + c.Data["commit_message"] = "" + c.Data["commit_choice"] = "direct" + c.Data["new_branch_name"] = "" + c.Data["last_commit"] = c.Repo.Commit.ID + c.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",") + c.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") + c.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",") + c.Data["EditorconfigURLPrefix"] = fmt.Sprintf("%s/api/v1/repos/%s/editorconfig/", setting.AppSubURL, c.Repo.Repository.FullName()) + + c.Success(EDIT_FILE) +} + +func EditFile(c *context.Context) { + editFile(c, false) +} + +func NewFile(c *context.Context) { + editFile(c, true) +} + +func editFilePost(c *context.Context, f form.EditRepoFile, isNewFile bool) { + c.PageIs("Edit") + c.RequireHighlightJS() + c.RequireSimpleMDE() + c.Data["IsNewFile"] = isNewFile + + oldBranchName := c.Repo.BranchName + branchName := oldBranchName + oldTreePath := c.Repo.TreePath + lastCommit := f.LastCommit + f.LastCommit = c.Repo.Commit.ID.String() + + if f.IsNewBrnach() { + branchName = f.NewBranchName + } + + f.TreePath = strings.Trim(f.TreePath, " /") + treeNames, treePaths := getParentTreeFields(f.TreePath) + + c.Data["ParentTreePath"] = path.Dir(c.Repo.TreePath) + c.Data["TreePath"] = f.TreePath + c.Data["TreeNames"] = treeNames + c.Data["TreePaths"] = treePaths + c.Data["BranchLink"] = c.Repo.RepoLink + "/src/" + branchName + c.Data["FileContent"] = f.Content + c.Data["commit_summary"] = f.CommitSummary + c.Data["commit_message"] = f.CommitMessage + c.Data["commit_choice"] = f.CommitChoice + c.Data["new_branch_name"] = branchName + c.Data["last_commit"] = f.LastCommit + c.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",") + c.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") + c.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",") + + if c.HasError() { + c.Success(EDIT_FILE) + return + } + + if len(f.TreePath) == 0 { + c.FormErr("TreePath") + c.RenderWithErr(c.Tr("repo.editor.filename_cannot_be_empty"), EDIT_FILE, &f) + return + } + + if oldBranchName != branchName { + if _, err := c.Repo.Repository.GetBranch(branchName); err == nil { + c.FormErr("NewBranchName") + c.RenderWithErr(c.Tr("repo.editor.branch_already_exists", branchName), EDIT_FILE, &f) + return + } + } + + var newTreePath string + for index, part := range treeNames { + newTreePath = path.Join(newTreePath, part) + entry, err := c.Repo.Commit.GetTreeEntryByPath(newTreePath) + if err != nil { + if git.IsErrNotExist(err) { + // Means there is no item with that name, so we're good + break + } + + c.ServerError("Repo.Commit.GetTreeEntryByPath", err) + return + } + if index != len(treeNames)-1 { + if !entry.IsDir() { + c.FormErr("TreePath") + c.RenderWithErr(c.Tr("repo.editor.directory_is_a_file", part), EDIT_FILE, &f) + return + } + } else { + if entry.IsLink() { + c.FormErr("TreePath") + c.RenderWithErr(c.Tr("repo.editor.file_is_a_symlink", part), EDIT_FILE, &f) + return + } else if entry.IsDir() { + c.FormErr("TreePath") + c.RenderWithErr(c.Tr("repo.editor.filename_is_a_directory", part), EDIT_FILE, &f) + return + } + } + } + + if !isNewFile { + _, err := c.Repo.Commit.GetTreeEntryByPath(oldTreePath) + if err != nil { + if git.IsErrNotExist(err) { + c.FormErr("TreePath") + c.RenderWithErr(c.Tr("repo.editor.file_editing_no_longer_exists", oldTreePath), EDIT_FILE, &f) + } else { + c.ServerError("GetTreeEntryByPath", err) + } + return + } + if lastCommit != c.Repo.CommitID { + files, err := c.Repo.Commit.GetFilesChangedSinceCommit(lastCommit) + if err != nil { + c.ServerError("GetFilesChangedSinceCommit", err) + return + } + + for _, file := range files { + if file == f.TreePath { + c.RenderWithErr(c.Tr("repo.editor.file_changed_while_editing", c.Repo.RepoLink+"/compare/"+lastCommit+"..."+c.Repo.CommitID), EDIT_FILE, &f) + return + } + } + } + } + + if oldTreePath != f.TreePath { + // We have a new filename (rename or completely new file) so we need to make sure it doesn't already exist, can't clobber. + entry, err := c.Repo.Commit.GetTreeEntryByPath(f.TreePath) + if err != nil { + if !git.IsErrNotExist(err) { + c.ServerError("GetTreeEntryByPath", err) + return + } + } + if entry != nil { + c.FormErr("TreePath") + c.RenderWithErr(c.Tr("repo.editor.file_already_exists", f.TreePath), EDIT_FILE, &f) + return + } + } + + message := strings.TrimSpace(f.CommitSummary) + if len(message) == 0 { + if isNewFile { + message = c.Tr("repo.editor.add", f.TreePath) + } else { + message = c.Tr("repo.editor.update", f.TreePath) + } + } + + f.CommitMessage = strings.TrimSpace(f.CommitMessage) + if len(f.CommitMessage) > 0 { + message += "\n\n" + f.CommitMessage + } + + if err := c.Repo.Repository.UpdateRepoFile(c.User, models.UpdateRepoFileOptions{ + LastCommitID: lastCommit, + OldBranch: oldBranchName, + NewBranch: branchName, + OldTreeName: oldTreePath, + NewTreeName: f.TreePath, + Message: message, + Content: strings.Replace(f.Content, "\r", "", -1), + IsNewFile: isNewFile, + }); err != nil { + c.FormErr("TreePath") + c.RenderWithErr(c.Tr("repo.editor.fail_to_update_file", f.TreePath, err), EDIT_FILE, &f) + return + } + + if f.IsNewBrnach() && c.Repo.PullRequest.Allowed { + c.Redirect(c.Repo.PullRequestURL(oldBranchName, f.NewBranchName)) + } else { + c.Redirect(c.Repo.RepoLink + "/src/" + branchName + "/" + template.EscapePound(f.TreePath)) + } +} + +func EditFilePost(c *context.Context, f form.EditRepoFile) { + editFilePost(c, f, false) +} + +func NewFilePost(c *context.Context, f form.EditRepoFile) { + editFilePost(c, f, true) +} + +func DiffPreviewPost(c *context.Context, f form.EditPreviewDiff) { + treePath := c.Repo.TreePath + + entry, err := c.Repo.Commit.GetTreeEntryByPath(treePath) + if err != nil { + c.Error(500, "GetTreeEntryByPath: "+err.Error()) + return + } else if entry.IsDir() { + c.Error(422) + return + } + + diff, err := c.Repo.Repository.GetDiffPreview(c.Repo.BranchName, treePath, f.Content) + if err != nil { + c.Error(500, "GetDiffPreview: "+err.Error()) + return + } + + if diff.NumFiles() == 0 { + c.PlainText(200, []byte(c.Tr("repo.editor.no_changes_to_show"))) + return + } + c.Data["File"] = diff.Files[0] + + c.HTML(200, EDIT_DIFF_PREVIEW) +} + +func DeleteFile(c *context.Context) { + c.Data["PageIsDelete"] = true + c.Data["BranchLink"] = c.Repo.RepoLink + "/src/" + c.Repo.BranchName + c.Data["TreePath"] = c.Repo.TreePath + c.Data["commit_summary"] = "" + c.Data["commit_message"] = "" + c.Data["commit_choice"] = "direct" + c.Data["new_branch_name"] = "" + c.HTML(200, DELETE_FILE) +} + +func DeleteFilePost(c *context.Context, f form.DeleteRepoFile) { + c.Data["PageIsDelete"] = true + c.Data["BranchLink"] = c.Repo.RepoLink + "/src/" + c.Repo.BranchName + c.Data["TreePath"] = c.Repo.TreePath + + oldBranchName := c.Repo.BranchName + branchName := oldBranchName + + if f.IsNewBrnach() { + branchName = f.NewBranchName + } + c.Data["commit_summary"] = f.CommitSummary + c.Data["commit_message"] = f.CommitMessage + c.Data["commit_choice"] = f.CommitChoice + c.Data["new_branch_name"] = branchName + + if c.HasError() { + c.HTML(200, DELETE_FILE) + return + } + + if oldBranchName != branchName { + if _, err := c.Repo.Repository.GetBranch(branchName); err == nil { + c.Data["Err_NewBranchName"] = true + c.RenderWithErr(c.Tr("repo.editor.branch_already_exists", branchName), DELETE_FILE, &f) + return + } + } + + message := strings.TrimSpace(f.CommitSummary) + if len(message) == 0 { + message = c.Tr("repo.editor.delete", c.Repo.TreePath) + } + + f.CommitMessage = strings.TrimSpace(f.CommitMessage) + if len(f.CommitMessage) > 0 { + message += "\n\n" + f.CommitMessage + } + + if err := c.Repo.Repository.DeleteRepoFile(c.User, models.DeleteRepoFileOptions{ + LastCommitID: c.Repo.CommitID, + OldBranch: oldBranchName, + NewBranch: branchName, + TreePath: c.Repo.TreePath, + Message: message, + }); err != nil { + c.Handle(500, "DeleteRepoFile", err) + return + } + + if f.IsNewBrnach() && c.Repo.PullRequest.Allowed { + c.Redirect(c.Repo.PullRequestURL(oldBranchName, f.NewBranchName)) + } else { + c.Flash.Success(c.Tr("repo.editor.file_delete_success", c.Repo.TreePath)) + c.Redirect(c.Repo.RepoLink + "/src/" + branchName) + } +} + +func renderUploadSettings(c *context.Context) { + c.Data["RequireDropzone"] = true + c.Data["UploadAllowedTypes"] = strings.Join(setting.Repository.Upload.AllowedTypes, ",") + c.Data["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize + c.Data["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles +} + +func UploadFile(c *context.Context) { + c.Data["PageIsUpload"] = true + renderUploadSettings(c) + + treeNames, treePaths := getParentTreeFields(c.Repo.TreePath) + if len(treeNames) == 0 { + // We must at least have one element for user to input. + treeNames = []string{""} + } + + c.Data["TreeNames"] = treeNames + c.Data["TreePaths"] = treePaths + c.Data["BranchLink"] = c.Repo.RepoLink + "/src/" + c.Repo.BranchName + c.Data["commit_summary"] = "" + c.Data["commit_message"] = "" + c.Data["commit_choice"] = "direct" + c.Data["new_branch_name"] = "" + + c.HTML(200, UPLOAD_FILE) +} + +func UploadFilePost(c *context.Context, f form.UploadRepoFile) { + c.Data["PageIsUpload"] = true + renderUploadSettings(c) + + oldBranchName := c.Repo.BranchName + branchName := oldBranchName + + if f.IsNewBrnach() { + branchName = f.NewBranchName + } + + f.TreePath = strings.Trim(f.TreePath, " /") + treeNames, treePaths := getParentTreeFields(f.TreePath) + if len(treeNames) == 0 { + // We must at least have one element for user to input. + treeNames = []string{""} + } + + c.Data["TreePath"] = f.TreePath + c.Data["TreeNames"] = treeNames + c.Data["TreePaths"] = treePaths + c.Data["BranchLink"] = c.Repo.RepoLink + "/src/" + branchName + c.Data["commit_summary"] = f.CommitSummary + c.Data["commit_message"] = f.CommitMessage + c.Data["commit_choice"] = f.CommitChoice + c.Data["new_branch_name"] = branchName + + if c.HasError() { + c.HTML(200, UPLOAD_FILE) + return + } + + if oldBranchName != branchName { + if _, err := c.Repo.Repository.GetBranch(branchName); err == nil { + c.Data["Err_NewBranchName"] = true + c.RenderWithErr(c.Tr("repo.editor.branch_already_exists", branchName), UPLOAD_FILE, &f) + return + } + } + + var newTreePath string + for _, part := range treeNames { + newTreePath = path.Join(newTreePath, part) + entry, err := c.Repo.Commit.GetTreeEntryByPath(newTreePath) + if err != nil { + if git.IsErrNotExist(err) { + // Means there is no item with that name, so we're good + break + } + + c.Handle(500, "Repo.Commit.GetTreeEntryByPath", err) + return + } + + // User can only upload files to a directory. + if !entry.IsDir() { + c.Data["Err_TreePath"] = true + c.RenderWithErr(c.Tr("repo.editor.directory_is_a_file", part), UPLOAD_FILE, &f) + return + } + } + + message := strings.TrimSpace(f.CommitSummary) + if len(message) == 0 { + message = c.Tr("repo.editor.upload_files_to_dir", f.TreePath) + } + + f.CommitMessage = strings.TrimSpace(f.CommitMessage) + if len(f.CommitMessage) > 0 { + message += "\n\n" + f.CommitMessage + } + + if err := c.Repo.Repository.UploadRepoFiles(c.User, models.UploadRepoFileOptions{ + LastCommitID: c.Repo.CommitID, + OldBranch: oldBranchName, + NewBranch: branchName, + TreePath: f.TreePath, + Message: message, + Files: f.Files, + }); err != nil { + c.Data["Err_TreePath"] = true + c.RenderWithErr(c.Tr("repo.editor.unable_to_upload_files", f.TreePath, err), UPLOAD_FILE, &f) + return + } + + if f.IsNewBrnach() && c.Repo.PullRequest.Allowed { + c.Redirect(c.Repo.PullRequestURL(oldBranchName, f.NewBranchName)) + } else { + c.Redirect(c.Repo.RepoLink + "/src/" + branchName + "/" + f.TreePath) + } +} + +func UploadFileToServer(c *context.Context) { + file, header, err := c.Req.FormFile("file") + if err != nil { + c.Error(500, fmt.Sprintf("FormFile: %v", err)) + return + } + defer file.Close() + + buf := make([]byte, 1024) + n, _ := file.Read(buf) + if n > 0 { + buf = buf[:n] + } + fileType := http.DetectContentType(buf) + + if len(setting.Repository.Upload.AllowedTypes) > 0 { + allowed := false + for _, t := range setting.Repository.Upload.AllowedTypes { + t := strings.Trim(t, " ") + if t == "*/*" || t == fileType { + allowed = true + break + } + } + + if !allowed { + c.Error(400, ErrFileTypeForbidden.Error()) + return + } + } + + upload, err := models.NewUpload(header.Filename, buf, file) + if err != nil { + c.Error(500, fmt.Sprintf("NewUpload: %v", err)) + return + } + + log.Trace("New file uploaded: %s", upload.UUID) + c.JSON(200, map[string]string{ + "uuid": upload.UUID, + }) +} + +func RemoveUploadFileFromServer(c *context.Context, f form.RemoveUploadFile) { + if len(f.File) == 0 { + c.Status(204) + return + } + + if err := models.DeleteUploadByUUID(f.File); err != nil { + c.Error(500, fmt.Sprintf("DeleteUploadByUUID: %v", err)) + return + } + + log.Trace("Upload file removed: %s", f.File) + c.Status(204) +} diff --git a/routes/repo/http.go b/routes/repo/http.go new file mode 100644 index 00000000..b8f519ba --- /dev/null +++ b/routes/repo/http.go @@ -0,0 +1,447 @@ +// Copyright 2017 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 ( + "bytes" + "compress/gzip" + "fmt" + "net/http" + "os" + "os/exec" + "path" + "regexp" + "strconv" + "strings" + "time" + + "github.com/Unknwon/com" + log "gopkg.in/clog.v1" + "gopkg.in/macaron.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/tool" +) + +const ( + ENV_AUTH_USER_ID = "GOGS_AUTH_USER_ID" + ENV_AUTH_USER_NAME = "GOGS_AUTH_USER_NAME" + ENV_AUTH_USER_EMAIL = "GOGS_AUTH_USER_EMAIL" + ENV_REPO_OWNER_NAME = "GOGS_REPO_OWNER_NAME" + ENV_REPO_OWNER_SALT_MD5 = "GOGS_REPO_OWNER_SALT_MD5" + ENV_REPO_ID = "GOGS_REPO_ID" + ENV_REPO_NAME = "GOGS_REPO_NAME" + ENV_REPO_CUSTOM_HOOKS_PATH = "GOGS_REPO_CUSTOM_HOOKS_PATH" +) + +type HTTPContext struct { + *context.Context + OwnerName string + OwnerSalt string + RepoID int64 + RepoName string + AuthUser *models.User +} + +// askCredentials responses HTTP header and status which informs client to provide credentials. +func askCredentials(c *context.Context, status int, text string) { + c.Resp.Header().Set("WWW-Authenticate", "Basic realm=\".\"") + c.HandleText(status, text) +} + +func HTTPContexter() macaron.Handler { + return func(c *context.Context) { + ownerName := c.Params(":username") + repoName := strings.TrimSuffix(c.Params(":reponame"), ".git") + repoName = strings.TrimSuffix(repoName, ".wiki") + + isPull := c.Query("service") == "git-upload-pack" || + strings.HasSuffix(c.Req.URL.Path, "git-upload-pack") || + c.Req.Method == "GET" + + owner, err := models.GetUserByName(ownerName) + if err != nil { + c.NotFoundOrServerError("GetUserByName", errors.IsUserNotExist, err) + return + } + + repo, err := models.GetRepositoryByName(owner.ID, repoName) + if err != nil { + c.NotFoundOrServerError("GetRepositoryByName", errors.IsRepoNotExist, err) + return + } + + // Authentication is not required for pulling from public repositories. + if isPull && !repo.IsPrivate && !setting.Service.RequireSignInView { + c.Map(&HTTPContext{ + Context: c, + }) + return + } + + // In case user requested a wrong URL and not intended to access Git objects. + action := c.Params("*") + if !strings.Contains(action, "git-") && + !strings.Contains(action, "info/") && + !strings.Contains(action, "HEAD") && + !strings.Contains(action, "objects/") { + c.NotFound() + return + } + + // Handle HTTP Basic Authentication + authHead := c.Req.Header.Get("Authorization") + if len(authHead) == 0 { + askCredentials(c, http.StatusUnauthorized, "") + return + } + + auths := strings.Fields(authHead) + if len(auths) != 2 || auths[0] != "Basic" { + askCredentials(c, http.StatusUnauthorized, "") + return + } + authUsername, authPassword, err := tool.BasicAuthDecode(auths[1]) + if err != nil { + askCredentials(c, http.StatusUnauthorized, "") + return + } + + authUser, err := models.UserSignIn(authUsername, authPassword) + if err != nil && !errors.IsUserNotExist(err) { + c.Handle(http.StatusInternalServerError, "UserSignIn", err) + return + } + + // If username and password combination failed, try again using username as a token. + if authUser == nil { + token, err := models.GetAccessTokenBySHA(authUsername) + if err != nil { + if models.IsErrAccessTokenEmpty(err) || models.IsErrAccessTokenNotExist(err) { + askCredentials(c, http.StatusUnauthorized, "") + } else { + c.Handle(http.StatusInternalServerError, "GetAccessTokenBySHA", err) + } + return + } + token.Updated = time.Now() + + authUser, err = models.GetUserByID(token.UID) + if err != nil { + // Once we found token, we're supposed to find its related user, + // thus any error is unexpected. + c.Handle(http.StatusInternalServerError, "GetUserByID", err) + return + } + } else if authUser.IsEnabledTwoFactor() { + askCredentials(c, http.StatusUnauthorized, `User with two-factor authentication enabled cannot perform HTTP/HTTPS operations via plain username and password +Please create and use personal access token on user settings page`) + return + } + + log.Trace("HTTPGit - Authenticated user: %s", authUser.Name) + + mode := models.ACCESS_MODE_WRITE + if isPull { + mode = models.ACCESS_MODE_READ + } + has, err := models.HasAccess(authUser.ID, repo, mode) + if err != nil { + c.Handle(http.StatusInternalServerError, "HasAccess", err) + return + } else if !has { + askCredentials(c, http.StatusForbidden, "User permission denied") + return + } + + if !isPull && repo.IsMirror { + c.HandleText(http.StatusForbidden, "Mirror repository is read-only") + return + } + + c.Map(&HTTPContext{ + Context: c, + OwnerName: ownerName, + OwnerSalt: owner.Salt, + RepoID: repo.ID, + RepoName: repoName, + AuthUser: authUser, + }) + } +} + +type serviceHandler struct { + w http.ResponseWriter + r *http.Request + dir string + file string + + authUser *models.User + ownerName string + ownerSalt string + repoID int64 + repoName string +} + +func (h *serviceHandler) setHeaderNoCache() { + h.w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT") + h.w.Header().Set("Pragma", "no-cache") + h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate") +} + +func (h *serviceHandler) setHeaderCacheForever() { + now := time.Now().Unix() + expires := now + 31536000 + h.w.Header().Set("Date", fmt.Sprintf("%d", now)) + h.w.Header().Set("Expires", fmt.Sprintf("%d", expires)) + h.w.Header().Set("Cache-Control", "public, max-age=31536000") +} + +func (h *serviceHandler) sendFile(contentType string) { + reqFile := path.Join(h.dir, h.file) + fi, err := os.Stat(reqFile) + if os.IsNotExist(err) { + h.w.WriteHeader(http.StatusNotFound) + return + } + + h.w.Header().Set("Content-Type", contentType) + h.w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size())) + h.w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat)) + http.ServeFile(h.w, h.r, reqFile) +} + +type ComposeHookEnvsOptions struct { + AuthUser *models.User + OwnerName string + OwnerSalt string + RepoID int64 + RepoName string + RepoPath string +} + +func ComposeHookEnvs(opts ComposeHookEnvsOptions) []string { + envs := []string{ + "SSH_ORIGINAL_COMMAND=1", + ENV_AUTH_USER_ID + "=" + com.ToStr(opts.AuthUser.ID), + ENV_AUTH_USER_NAME + "=" + opts.AuthUser.Name, + ENV_AUTH_USER_EMAIL + "=" + opts.AuthUser.Email, + ENV_REPO_OWNER_NAME + "=" + opts.OwnerName, + ENV_REPO_OWNER_SALT_MD5 + "=" + tool.MD5(opts.OwnerSalt), + ENV_REPO_ID + "=" + com.ToStr(opts.RepoID), + ENV_REPO_NAME + "=" + opts.RepoName, + ENV_REPO_CUSTOM_HOOKS_PATH + "=" + path.Join(opts.RepoPath, "custom_hooks"), + } + return envs +} + +func serviceRPC(h serviceHandler, service string) { + defer h.r.Body.Close() + + if h.r.Header.Get("Content-Type") != fmt.Sprintf("application/x-git-%s-request", service) { + h.w.WriteHeader(http.StatusUnauthorized) + return + } + h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service)) + + var ( + reqBody = h.r.Body + err error + ) + + // Handle GZIP + if h.r.Header.Get("Content-Encoding") == "gzip" { + reqBody, err = gzip.NewReader(reqBody) + if err != nil { + log.Error(2, "HTTP.Get: fail to create gzip reader: %v", err) + h.w.WriteHeader(http.StatusInternalServerError) + return + } + } + + var stderr bytes.Buffer + cmd := exec.Command("git", service, "--stateless-rpc", h.dir) + if service == "receive-pack" { + cmd.Env = append(os.Environ(), ComposeHookEnvs(ComposeHookEnvsOptions{ + AuthUser: h.authUser, + OwnerName: h.ownerName, + OwnerSalt: h.ownerSalt, + RepoID: h.repoID, + RepoName: h.repoName, + RepoPath: h.dir, + })...) + } + cmd.Dir = h.dir + cmd.Stdout = h.w + cmd.Stderr = &stderr + cmd.Stdin = reqBody + if err = cmd.Run(); err != nil { + log.Error(2, "HTTP.serviceRPC: fail to serve RPC '%s': %v - %s", service, err, stderr) + h.w.WriteHeader(http.StatusInternalServerError) + return + } +} + +func serviceUploadPack(h serviceHandler) { + serviceRPC(h, "upload-pack") +} + +func serviceReceivePack(h serviceHandler) { + serviceRPC(h, "receive-pack") +} + +func getServiceType(r *http.Request) string { + serviceType := r.FormValue("service") + if !strings.HasPrefix(serviceType, "git-") { + return "" + } + return strings.TrimPrefix(serviceType, "git-") +} + +// FIXME: use process module +func gitCommand(dir string, args ...string) []byte { + cmd := exec.Command("git", args...) + cmd.Dir = dir + out, err := cmd.Output() + if err != nil { + log.Error(2, fmt.Sprintf("Git: %v - %s", err, out)) + } + return out +} + +func updateServerInfo(dir string) []byte { + return gitCommand(dir, "update-server-info") +} + +func packetWrite(str string) []byte { + s := strconv.FormatInt(int64(len(str)+4), 16) + if len(s)%4 != 0 { + s = strings.Repeat("0", 4-len(s)%4) + s + } + return []byte(s + str) +} + +func getInfoRefs(h serviceHandler) { + h.setHeaderNoCache() + service := getServiceType(h.r) + if service != "upload-pack" && service != "receive-pack" { + updateServerInfo(h.dir) + h.sendFile("text/plain; charset=utf-8") + return + } + + refs := gitCommand(h.dir, service, "--stateless-rpc", "--advertise-refs", ".") + h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service)) + h.w.WriteHeader(http.StatusOK) + h.w.Write(packetWrite("# service=git-" + service + "\n")) + h.w.Write([]byte("0000")) + h.w.Write(refs) +} + +func getTextFile(h serviceHandler) { + h.setHeaderNoCache() + h.sendFile("text/plain") +} + +func getInfoPacks(h serviceHandler) { + h.setHeaderCacheForever() + h.sendFile("text/plain; charset=utf-8") +} + +func getLooseObject(h serviceHandler) { + h.setHeaderCacheForever() + h.sendFile("application/x-git-loose-object") +} + +func getPackFile(h serviceHandler) { + h.setHeaderCacheForever() + h.sendFile("application/x-git-packed-objects") +} + +func getIdxFile(h serviceHandler) { + h.setHeaderCacheForever() + h.sendFile("application/x-git-packed-objects-toc") +} + +var routes = []struct { + reg *regexp.Regexp + method string + handler func(serviceHandler) +}{ + {regexp.MustCompile("(.*?)/git-upload-pack$"), "POST", serviceUploadPack}, + {regexp.MustCompile("(.*?)/git-receive-pack$"), "POST", serviceReceivePack}, + {regexp.MustCompile("(.*?)/info/refs$"), "GET", getInfoRefs}, + {regexp.MustCompile("(.*?)/HEAD$"), "GET", getTextFile}, + {regexp.MustCompile("(.*?)/objects/info/alternates$"), "GET", getTextFile}, + {regexp.MustCompile("(.*?)/objects/info/http-alternates$"), "GET", getTextFile}, + {regexp.MustCompile("(.*?)/objects/info/packs$"), "GET", getInfoPacks}, + {regexp.MustCompile("(.*?)/objects/info/[^/]*$"), "GET", getTextFile}, + {regexp.MustCompile("(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$"), "GET", getLooseObject}, + {regexp.MustCompile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.pack$"), "GET", getPackFile}, + {regexp.MustCompile("(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$"), "GET", getIdxFile}, +} + +func getGitRepoPath(dir string) (string, error) { + if !strings.HasSuffix(dir, ".git") { + dir += ".git" + } + + filename := path.Join(setting.RepoRootPath, dir) + if _, err := os.Stat(filename); os.IsNotExist(err) { + return "", err + } + + return filename, nil +} + +func HTTP(c *HTTPContext) { + for _, route := range routes { + reqPath := strings.ToLower(c.Req.URL.Path) + m := route.reg.FindStringSubmatch(reqPath) + if m == nil { + continue + } + + // We perform check here because routes matched in cmd/web.go is wider than needed, + // but we only want to output this message only if user is really trying to access + // Git HTTP endpoints. + if setting.Repository.DisableHTTPGit { + c.HandleText(http.StatusForbidden, "Interacting with repositories by HTTP protocol is not disabled") + return + } + + if route.method != c.Req.Method { + c.NotFound() + return + } + + file := strings.TrimPrefix(reqPath, m[1]+"/") + dir, err := getGitRepoPath(m[1]) + if err != nil { + log.Warn("HTTP.getGitRepoPath: %v", err) + c.NotFound() + return + } + + route.handler(serviceHandler{ + w: c.Resp, + r: c.Req.Request, + dir: dir, + file: file, + + authUser: c.AuthUser, + ownerName: c.OwnerName, + ownerSalt: c.OwnerSalt, + repoID: c.RepoID, + repoName: c.RepoName, + }) + return + } + + c.NotFound() +} diff --git a/routes/repo/issue.go b/routes/repo/issue.go new file mode 100644 index 00000000..8920bc32 --- /dev/null +++ b/routes/repo/issue.go @@ -0,0 +1,1263 @@ +// 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" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" + + "github.com/Unknwon/com" + "github.com/Unknwon/paginater" + log "gopkg.in/clog.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/markup" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/tool" +) + +const ( + ISSUES = "repo/issue/list" + ISSUE_NEW = "repo/issue/new" + ISSUE_VIEW = "repo/issue/view" + + LABELS = "repo/issue/labels" + + MILESTONE = "repo/issue/milestones" + MILESTONE_NEW = "repo/issue/milestone_new" + MILESTONE_EDIT = "repo/issue/milestone_edit" + + ISSUE_TEMPLATE_KEY = "IssueTemplate" +) + +var ( + ErrFileTypeForbidden = errors.New("File type is not allowed") + ErrTooManyFiles = errors.New("Maximum number of files to upload exceeded") + + IssueTemplateCandidates = []string{ + "ISSUE_TEMPLATE.md", + ".gogs/ISSUE_TEMPLATE.md", + ".github/ISSUE_TEMPLATE.md", + } +) + +func MustEnableIssues(c *context.Context) { + if !c.Repo.Repository.EnableIssues { + c.Handle(404, "MustEnableIssues", nil) + return + } + + if c.Repo.Repository.EnableExternalTracker { + c.Redirect(c.Repo.Repository.ExternalTrackerURL) + return + } +} + +func MustAllowPulls(c *context.Context) { + if !c.Repo.Repository.AllowsPulls() { + c.Handle(404, "MustAllowPulls", nil) + return + } + + // User can send pull request if owns a forked repository. + if c.IsLogged && c.User.HasForkedRepo(c.Repo.Repository.ID) { + c.Repo.PullRequest.Allowed = true + c.Repo.PullRequest.HeadInfo = c.User.Name + ":" + c.Repo.BranchName + } +} + +func RetrieveLabels(c *context.Context) { + labels, err := models.GetLabelsByRepoID(c.Repo.Repository.ID) + if err != nil { + c.Handle(500, "RetrieveLabels.GetLabels", err) + return + } + for _, l := range labels { + l.CalOpenIssues() + } + c.Data["Labels"] = labels + c.Data["NumLabels"] = len(labels) +} + +func issues(c *context.Context, isPullList bool) { + if isPullList { + MustAllowPulls(c) + if c.Written() { + return + } + c.Data["Title"] = c.Tr("repo.pulls") + c.Data["PageIsPullList"] = true + + } else { + MustEnableIssues(c) + if c.Written() { + return + } + c.Data["Title"] = c.Tr("repo.issues") + c.Data["PageIsIssueList"] = true + } + + viewType := c.Query("type") + sortType := c.Query("sort") + types := []string{"assigned", "created_by", "mentioned"} + if !com.IsSliceContainsStr(types, viewType) { + viewType = "all" + } + + // Must sign in to see issues about you. + if viewType != "all" && !c.IsLogged { + c.SetCookie("redirect_to", "/"+url.QueryEscape(setting.AppSubURL+c.Req.RequestURI), 0, setting.AppSubURL) + c.Redirect(setting.AppSubURL + "/user/login") + return + } + + var ( + assigneeID = c.QueryInt64("assignee") + posterID int64 + ) + filterMode := models.FILTER_MODE_YOUR_REPOS + switch viewType { + case "assigned": + filterMode = models.FILTER_MODE_ASSIGN + assigneeID = c.User.ID + case "created_by": + filterMode = models.FILTER_MODE_CREATE + posterID = c.User.ID + case "mentioned": + filterMode = models.FILTER_MODE_MENTION + } + + var uid int64 = -1 + if c.IsLogged { + uid = c.User.ID + } + + repo := c.Repo.Repository + selectLabels := c.Query("labels") + milestoneID := c.QueryInt64("milestone") + isShowClosed := c.Query("state") == "closed" + issueStats := models.GetIssueStats(&models.IssueStatsOptions{ + RepoID: repo.ID, + UserID: uid, + Labels: selectLabels, + MilestoneID: milestoneID, + AssigneeID: assigneeID, + FilterMode: filterMode, + IsPull: isPullList, + }) + + page := c.QueryInt("page") + if page <= 1 { + page = 1 + } + + var total int + if !isShowClosed { + total = int(issueStats.OpenCount) + } else { + total = int(issueStats.ClosedCount) + } + pager := paginater.New(total, setting.UI.IssuePagingNum, page, 5) + c.Data["Page"] = pager + + issues, err := models.Issues(&models.IssuesOptions{ + UserID: uid, + AssigneeID: assigneeID, + RepoID: repo.ID, + PosterID: posterID, + MilestoneID: milestoneID, + Page: pager.Current(), + IsClosed: isShowClosed, + IsMention: filterMode == models.FILTER_MODE_MENTION, + IsPull: isPullList, + Labels: selectLabels, + SortType: sortType, + }) + if err != nil { + c.Handle(500, "Issues", err) + return + } + + // Get issue-user relations. + pairs, err := models.GetIssueUsers(repo.ID, posterID, isShowClosed) + if err != nil { + c.Handle(500, "GetIssueUsers", err) + return + } + + // Get posters. + for i := range issues { + if !c.IsLogged { + issues[i].IsRead = true + continue + } + + // Check read status. + idx := models.PairsContains(pairs, issues[i].ID, c.User.ID) + if idx > -1 { + issues[i].IsRead = pairs[idx].IsRead + } else { + issues[i].IsRead = true + } + } + c.Data["Issues"] = issues + + // Get milestones. + c.Data["Milestones"], err = models.GetMilestonesByRepoID(repo.ID) + if err != nil { + c.Handle(500, "GetAllRepoMilestones", err) + return + } + + // Get assignees. + c.Data["Assignees"], err = repo.GetAssignees() + if err != nil { + c.Handle(500, "GetAssignees", err) + return + } + + if viewType == "assigned" { + assigneeID = 0 // Reset ID to prevent unexpected selection of assignee. + } + + c.Data["IssueStats"] = issueStats + c.Data["SelectLabels"] = com.StrTo(selectLabels).MustInt64() + c.Data["ViewType"] = viewType + c.Data["SortType"] = sortType + c.Data["MilestoneID"] = milestoneID + c.Data["AssigneeID"] = assigneeID + c.Data["IsShowClosed"] = isShowClosed + if isShowClosed { + c.Data["State"] = "closed" + } else { + c.Data["State"] = "open" + } + + c.HTML(200, ISSUES) +} + +func Issues(c *context.Context) { + issues(c, false) +} + +func Pulls(c *context.Context) { + issues(c, true) +} + +func renderAttachmentSettings(c *context.Context) { + c.Data["RequireDropzone"] = true + c.Data["IsAttachmentEnabled"] = setting.AttachmentEnabled + c.Data["AttachmentAllowedTypes"] = setting.AttachmentAllowedTypes + c.Data["AttachmentMaxSize"] = setting.AttachmentMaxSize + c.Data["AttachmentMaxFiles"] = setting.AttachmentMaxFiles +} + +func RetrieveRepoMilestonesAndAssignees(c *context.Context, repo *models.Repository) { + var err error + c.Data["OpenMilestones"], err = models.GetMilestones(repo.ID, -1, false) + if err != nil { + c.Handle(500, "GetMilestones", err) + return + } + c.Data["ClosedMilestones"], err = models.GetMilestones(repo.ID, -1, true) + if err != nil { + c.Handle(500, "GetMilestones", err) + return + } + + c.Data["Assignees"], err = repo.GetAssignees() + if err != nil { + c.Handle(500, "GetAssignees", err) + return + } +} + +func RetrieveRepoMetas(c *context.Context, repo *models.Repository) []*models.Label { + if !c.Repo.IsWriter() { + return nil + } + + labels, err := models.GetLabelsByRepoID(repo.ID) + if err != nil { + c.Handle(500, "GetLabelsByRepoID", err) + return nil + } + c.Data["Labels"] = labels + + RetrieveRepoMilestonesAndAssignees(c, repo) + if c.Written() { + return nil + } + + return labels +} + +func getFileContentFromDefaultBranch(c *context.Context, filename string) (string, bool) { + var r io.Reader + var bytes []byte + + if c.Repo.Commit == nil { + var err error + c.Repo.Commit, err = c.Repo.GitRepo.GetBranchCommit(c.Repo.Repository.DefaultBranch) + if err != nil { + return "", false + } + } + + entry, err := c.Repo.Commit.GetTreeEntryByPath(filename) + if err != nil { + return "", false + } + r, err = entry.Blob().Data() + if err != nil { + return "", false + } + bytes, err = ioutil.ReadAll(r) + if err != nil { + return "", false + } + return string(bytes), true +} + +func setTemplateIfExists(c *context.Context, ctxDataKey string, possibleFiles []string) { + for _, filename := range possibleFiles { + content, found := getFileContentFromDefaultBranch(c, filename) + if found { + c.Data[ctxDataKey] = content + return + } + } +} + +func NewIssue(c *context.Context) { + c.Data["Title"] = c.Tr("repo.issues.new") + c.Data["PageIsIssueList"] = true + c.Data["RequireHighlightJS"] = true + c.Data["RequireSimpleMDE"] = true + setTemplateIfExists(c, ISSUE_TEMPLATE_KEY, IssueTemplateCandidates) + renderAttachmentSettings(c) + + RetrieveRepoMetas(c, c.Repo.Repository) + if c.Written() { + return + } + + c.HTML(200, ISSUE_NEW) +} + +func ValidateRepoMetas(c *context.Context, f form.NewIssue) ([]int64, int64, int64) { + var ( + repo = c.Repo.Repository + err error + ) + + labels := RetrieveRepoMetas(c, c.Repo.Repository) + if c.Written() { + return nil, 0, 0 + } + + if !c.Repo.IsWriter() { + return nil, 0, 0 + } + + // Check labels. + labelIDs := tool.StringsToInt64s(strings.Split(f.LabelIDs, ",")) + labelIDMark := tool.Int64sToMap(labelIDs) + hasSelected := false + for i := range labels { + if labelIDMark[labels[i].ID] { + labels[i].IsChecked = true + hasSelected = true + } + } + c.Data["HasSelectedLabel"] = hasSelected + c.Data["label_ids"] = f.LabelIDs + c.Data["Labels"] = labels + + // Check milestone. + milestoneID := f.MilestoneID + if milestoneID > 0 { + c.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID) + if err != nil { + c.Handle(500, "GetMilestoneByID", err) + return nil, 0, 0 + } + c.Data["milestone_id"] = milestoneID + } + + // Check assignee. + assigneeID := f.AssigneeID + if assigneeID > 0 { + c.Data["Assignee"], err = repo.GetAssigneeByID(assigneeID) + if err != nil { + c.Handle(500, "GetAssigneeByID", err) + return nil, 0, 0 + } + c.Data["assignee_id"] = assigneeID + } + + return labelIDs, milestoneID, assigneeID +} + +func NewIssuePost(c *context.Context, f form.NewIssue) { + c.Data["Title"] = c.Tr("repo.issues.new") + c.Data["PageIsIssueList"] = true + c.Data["RequireHighlightJS"] = true + c.Data["RequireSimpleMDE"] = true + renderAttachmentSettings(c) + + labelIDs, milestoneID, assigneeID := ValidateRepoMetas(c, f) + if c.Written() { + return + } + + if c.HasError() { + c.HTML(200, ISSUE_NEW) + return + } + + var attachments []string + if setting.AttachmentEnabled { + attachments = f.Files + } + + issue := &models.Issue{ + RepoID: c.Repo.Repository.ID, + Title: f.Title, + PosterID: c.User.ID, + Poster: c.User, + MilestoneID: milestoneID, + AssigneeID: assigneeID, + Content: f.Content, + } + if err := models.NewIssue(c.Repo.Repository, issue, labelIDs, attachments); err != nil { + c.Handle(500, "NewIssue", err) + return + } + + log.Trace("Issue created: %d/%d", c.Repo.Repository.ID, issue.ID) + c.Redirect(c.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index)) +} + +func uploadAttachment(c *context.Context, allowedTypes []string) { + file, header, err := c.Req.FormFile("file") + if err != nil { + c.Error(500, fmt.Sprintf("FormFile: %v", err)) + return + } + defer file.Close() + + buf := make([]byte, 1024) + n, _ := file.Read(buf) + if n > 0 { + buf = buf[:n] + } + fileType := http.DetectContentType(buf) + + allowed := false + for _, t := range allowedTypes { + t := strings.Trim(t, " ") + if t == "*/*" || t == fileType { + allowed = true + break + } + } + + if !allowed { + c.Error(400, ErrFileTypeForbidden.Error()) + return + } + + attach, err := models.NewAttachment(header.Filename, buf, file) + if err != nil { + c.Error(500, fmt.Sprintf("NewAttachment: %v", err)) + return + } + + log.Trace("New attachment uploaded: %s", attach.UUID) + c.JSON(200, map[string]string{ + "uuid": attach.UUID, + }) +} + +func UploadIssueAttachment(c *context.Context) { + if !setting.AttachmentEnabled { + c.NotFound() + return + } + + uploadAttachment(c, strings.Split(setting.AttachmentAllowedTypes, ",")) +} + +func viewIssue(c *context.Context, isPullList bool) { + c.Data["RequireHighlightJS"] = true + c.Data["RequireDropzone"] = true + renderAttachmentSettings(c) + + index := c.ParamsInt64(":index") + if index <= 0 { + c.NotFound() + return + } + + issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, index) + if err != nil { + c.NotFoundOrServerError("GetIssueByIndex", errors.IsIssueNotExist, err) + return + } + c.Data["Title"] = issue.Title + + // Make sure type and URL matches. + if !isPullList && issue.IsPull { + c.Redirect(c.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index)) + return + } else if isPullList && !issue.IsPull { + c.Redirect(c.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index)) + return + } + + if issue.IsPull { + MustAllowPulls(c) + if c.Written() { + return + } + c.Data["PageIsPullList"] = true + c.Data["PageIsPullConversation"] = true + } else { + MustEnableIssues(c) + if c.Written() { + return + } + c.Data["PageIsIssueList"] = true + } + + issue.RenderedContent = string(markup.Markdown(issue.Content, c.Repo.RepoLink, c.Repo.Repository.ComposeMetas())) + + repo := c.Repo.Repository + + // Get more information if it's a pull request. + if issue.IsPull { + if issue.PullRequest.HasMerged { + c.Data["DisableStatusChange"] = issue.PullRequest.HasMerged + PrepareMergedViewPullInfo(c, issue) + } else { + PrepareViewPullInfo(c, issue) + } + if c.Written() { + return + } + } + + // Metas. + // Check labels. + labelIDMark := make(map[int64]bool) + for i := range issue.Labels { + labelIDMark[issue.Labels[i].ID] = true + } + labels, err := models.GetLabelsByRepoID(repo.ID) + if err != nil { + c.Handle(500, "GetLabelsByRepoID", err) + return + } + hasSelected := false + for i := range labels { + if labelIDMark[labels[i].ID] { + labels[i].IsChecked = true + hasSelected = true + } + } + c.Data["HasSelectedLabel"] = hasSelected + c.Data["Labels"] = labels + + // Check milestone and assignee. + if c.Repo.IsWriter() { + RetrieveRepoMilestonesAndAssignees(c, repo) + if c.Written() { + return + } + } + + if c.IsLogged { + // Update issue-user. + if err = issue.ReadBy(c.User.ID); err != nil { + c.Handle(500, "ReadBy", err) + return + } + } + + var ( + tag models.CommentTag + ok bool + marked = make(map[int64]models.CommentTag) + comment *models.Comment + participants = make([]*models.User, 1, 10) + ) + + // Render comments and and fetch participants. + participants[0] = issue.Poster + for _, comment = range issue.Comments { + if comment.Type == models.COMMENT_TYPE_COMMENT { + comment.RenderedContent = string(markup.Markdown(comment.Content, c.Repo.RepoLink, c.Repo.Repository.ComposeMetas())) + + // Check tag. + tag, ok = marked[comment.PosterID] + if ok { + comment.ShowTag = tag + continue + } + + if repo.IsOwnedBy(comment.PosterID) || + (repo.Owner.IsOrganization() && repo.Owner.IsOwnedBy(comment.PosterID)) { + comment.ShowTag = models.COMMENT_TAG_OWNER + } else if comment.Poster.IsWriterOfRepo(repo) { + comment.ShowTag = models.COMMENT_TAG_WRITER + } else if comment.PosterID == issue.PosterID { + comment.ShowTag = models.COMMENT_TAG_POSTER + } + + marked[comment.PosterID] = comment.ShowTag + + isAdded := false + for j := range participants { + if comment.Poster == participants[j] { + isAdded = true + break + } + } + if !isAdded && !issue.IsPoster(comment.Poster.ID) { + participants = append(participants, comment.Poster) + } + } + } + + if issue.IsPull && issue.PullRequest.HasMerged { + pull := issue.PullRequest + c.Data["IsPullBranchDeletable"] = pull.BaseRepoID == pull.HeadRepoID && + c.Repo.IsWriter() && c.Repo.GitRepo.IsBranchExist(pull.HeadBranch) + + deleteBranchUrl := c.Repo.RepoLink + "/branches/delete/" + pull.HeadBranch + c.Data["DeleteBranchLink"] = fmt.Sprintf("%s?commit=%s&redirect_to=%s", deleteBranchUrl, pull.MergedCommitID, c.Data["Link"]) + } + + c.Data["Participants"] = participants + c.Data["NumParticipants"] = len(participants) + c.Data["Issue"] = issue + c.Data["IsIssueOwner"] = c.Repo.IsWriter() || (c.IsLogged && issue.IsPoster(c.User.ID)) + c.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + c.Data["Link"].(string) + c.HTML(200, ISSUE_VIEW) +} + +func ViewIssue(c *context.Context) { + viewIssue(c, false) +} + +func ViewPull(c *context.Context) { + viewIssue(c, true) +} + +func getActionIssue(c *context.Context) *models.Issue { + issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + c.NotFoundOrServerError("GetIssueByIndex", errors.IsIssueNotExist, err) + return nil + } + + // Prevent guests accessing pull requests + if !c.Repo.HasAccess() && issue.IsPull { + c.NotFound() + return nil + } + + return issue +} + +func UpdateIssueTitle(c *context.Context) { + issue := getActionIssue(c) + if c.Written() { + return + } + + if !c.IsLogged || (!issue.IsPoster(c.User.ID) && !c.Repo.IsWriter()) { + c.Error(403) + return + } + + title := c.QueryTrim("title") + if len(title) == 0 { + c.Error(204) + return + } + + if err := issue.ChangeTitle(c.User, title); err != nil { + c.Handle(500, "ChangeTitle", err) + return + } + + c.JSON(200, map[string]interface{}{ + "title": issue.Title, + }) +} + +func UpdateIssueContent(c *context.Context) { + issue := getActionIssue(c) + if c.Written() { + return + } + + if !c.IsLogged || (c.User.ID != issue.PosterID && !c.Repo.IsWriter()) { + c.Error(403) + return + } + + content := c.Query("content") + if err := issue.ChangeContent(c.User, content); err != nil { + c.Handle(500, "ChangeContent", err) + return + } + + c.JSON(200, map[string]string{ + "content": string(markup.Markdown(issue.Content, c.Query("context"), c.Repo.Repository.ComposeMetas())), + }) +} + +func UpdateIssueLabel(c *context.Context) { + issue := getActionIssue(c) + if c.Written() { + return + } + + if c.Query("action") == "clear" { + if err := issue.ClearLabels(c.User); err != nil { + c.Handle(500, "ClearLabels", err) + return + } + } else { + isAttach := c.Query("action") == "attach" + label, err := models.GetLabelOfRepoByID(c.Repo.Repository.ID, c.QueryInt64("id")) + if err != nil { + if models.IsErrLabelNotExist(err) { + c.Error(404, "GetLabelByID") + } else { + c.Handle(500, "GetLabelByID", err) + } + return + } + + if isAttach && !issue.HasLabel(label.ID) { + if err = issue.AddLabel(c.User, label); err != nil { + c.Handle(500, "AddLabel", err) + return + } + } else if !isAttach && issue.HasLabel(label.ID) { + if err = issue.RemoveLabel(c.User, label); err != nil { + c.Handle(500, "RemoveLabel", err) + return + } + } + } + + c.JSON(200, map[string]interface{}{ + "ok": true, + }) +} + +func UpdateIssueMilestone(c *context.Context) { + issue := getActionIssue(c) + if c.Written() { + return + } + + oldMilestoneID := issue.MilestoneID + milestoneID := c.QueryInt64("id") + if oldMilestoneID == milestoneID { + c.JSON(200, map[string]interface{}{ + "ok": true, + }) + return + } + + // Not check for invalid milestone id and give responsibility to owners. + issue.MilestoneID = milestoneID + if err := models.ChangeMilestoneAssign(c.User, issue, oldMilestoneID); err != nil { + c.Handle(500, "ChangeMilestoneAssign", err) + return + } + + c.JSON(200, map[string]interface{}{ + "ok": true, + }) +} + +func UpdateIssueAssignee(c *context.Context) { + issue := getActionIssue(c) + if c.Written() { + return + } + + assigneeID := c.QueryInt64("id") + if issue.AssigneeID == assigneeID { + c.JSON(200, map[string]interface{}{ + "ok": true, + }) + return + } + + if err := issue.ChangeAssignee(c.User, assigneeID); err != nil { + c.Handle(500, "ChangeAssignee", err) + return + } + + c.JSON(200, map[string]interface{}{ + "ok": true, + }) +} + +func NewComment(c *context.Context, f form.CreateComment) { + issue := getActionIssue(c) + if c.Written() { + return + } + + var attachments []string + if setting.AttachmentEnabled { + attachments = f.Files + } + + if c.HasError() { + c.Flash.Error(c.Data["ErrorMsg"].(string)) + c.Redirect(fmt.Sprintf("%s/issues/%d", c.Repo.RepoLink, issue.Index)) + return + } + + var err error + var comment *models.Comment + defer func() { + // Check if issue admin/poster changes the status of issue. + if (c.Repo.IsWriter() || (c.IsLogged && issue.IsPoster(c.User.ID))) && + (f.Status == "reopen" || f.Status == "close") && + !(issue.IsPull && issue.PullRequest.HasMerged) { + + // Duplication and conflict check should apply to reopen pull request. + var pr *models.PullRequest + + if f.Status == "reopen" && issue.IsPull { + pull := issue.PullRequest + pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch) + if err != nil { + if !models.IsErrPullRequestNotExist(err) { + c.ServerError("GetUnmergedPullRequest", err) + return + } + } + + // Regenerate patch and test conflict. + if pr == nil { + if err = issue.PullRequest.UpdatePatch(); err != nil { + c.ServerError("UpdatePatch", err) + return + } + + issue.PullRequest.AddToTaskQueue() + } + } + + if pr != nil { + c.Flash.Info(c.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index)) + } else { + if err = issue.ChangeStatus(c.User, c.Repo.Repository, f.Status == "close"); err != nil { + log.Error(2, "ChangeStatus: %v", err) + } else { + log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed) + } + } + } + + // Redirect to comment hashtag if there is any actual content. + typeName := "issues" + if issue.IsPull { + typeName = "pulls" + } + if comment != nil { + c.Redirect(fmt.Sprintf("%s/%s/%d#%s", c.Repo.RepoLink, typeName, issue.Index, comment.HashTag())) + } else { + c.Redirect(fmt.Sprintf("%s/%s/%d", c.Repo.RepoLink, typeName, issue.Index)) + } + }() + + // Fix #321: Allow empty comments, as long as we have attachments. + if len(f.Content) == 0 && len(attachments) == 0 { + return + } + + comment, err = models.CreateIssueComment(c.User, c.Repo.Repository, issue, f.Content, attachments) + if err != nil { + c.ServerError("CreateIssueComment", err) + return + } + + log.Trace("Comment created: %d/%d/%d", c.Repo.Repository.ID, issue.ID, comment.ID) +} + +func UpdateCommentContent(c *context.Context) { + comment, err := models.GetCommentByID(c.ParamsInt64(":id")) + if err != nil { + c.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err) + return + } + + if c.UserID() != comment.PosterID && !c.Repo.IsAdmin() { + c.Error(404) + return + } else if comment.Type != models.COMMENT_TYPE_COMMENT { + c.Error(204) + return + } + + oldContent := comment.Content + comment.Content = c.Query("content") + if len(comment.Content) == 0 { + c.JSON(200, map[string]interface{}{ + "content": "", + }) + return + } + if err = models.UpdateComment(c.User, comment, oldContent); err != nil { + c.Handle(500, "UpdateComment", err) + return + } + + c.JSON(200, map[string]string{ + "content": string(markup.Markdown(comment.Content, c.Query("context"), c.Repo.Repository.ComposeMetas())), + }) +} + +func DeleteComment(c *context.Context) { + comment, err := models.GetCommentByID(c.ParamsInt64(":id")) + if err != nil { + c.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err) + return + } + + if c.UserID() != comment.PosterID && !c.Repo.IsAdmin() { + c.Error(404) + return + } else if comment.Type != models.COMMENT_TYPE_COMMENT { + c.Error(204) + return + } + + if err = models.DeleteCommentByID(c.User, comment.ID); err != nil { + c.Handle(500, "DeleteCommentByID", err) + return + } + + c.Status(200) +} + +func Labels(c *context.Context) { + c.Data["Title"] = c.Tr("repo.labels") + c.Data["PageIsIssueList"] = true + c.Data["PageIsLabels"] = true + c.Data["RequireMinicolors"] = true + c.Data["LabelTemplates"] = models.LabelTemplates + c.HTML(200, LABELS) +} + +func InitializeLabels(c *context.Context, f form.InitializeLabels) { + if c.HasError() { + c.Redirect(c.Repo.RepoLink + "/labels") + return + } + list, err := models.GetLabelTemplateFile(f.TemplateName) + if err != nil { + c.Flash.Error(c.Tr("repo.issues.label_templates.fail_to_load_file", f.TemplateName, err)) + c.Redirect(c.Repo.RepoLink + "/labels") + return + } + + labels := make([]*models.Label, len(list)) + for i := 0; i < len(list); i++ { + labels[i] = &models.Label{ + RepoID: c.Repo.Repository.ID, + Name: list[i][0], + Color: list[i][1], + } + } + if err := models.NewLabels(labels...); err != nil { + c.Handle(500, "NewLabels", err) + return + } + c.Redirect(c.Repo.RepoLink + "/labels") +} + +func NewLabel(c *context.Context, f form.CreateLabel) { + c.Data["Title"] = c.Tr("repo.labels") + c.Data["PageIsLabels"] = true + + if c.HasError() { + c.Flash.Error(c.Data["ErrorMsg"].(string)) + c.Redirect(c.Repo.RepoLink + "/labels") + return + } + + l := &models.Label{ + RepoID: c.Repo.Repository.ID, + Name: f.Title, + Color: f.Color, + } + if err := models.NewLabels(l); err != nil { + c.Handle(500, "NewLabel", err) + return + } + c.Redirect(c.Repo.RepoLink + "/labels") +} + +func UpdateLabel(c *context.Context, f form.CreateLabel) { + l, err := models.GetLabelByID(f.ID) + if err != nil { + switch { + case models.IsErrLabelNotExist(err): + c.Error(404) + default: + c.Handle(500, "UpdateLabel", err) + } + return + } + + l.Name = f.Title + l.Color = f.Color + if err := models.UpdateLabel(l); err != nil { + c.Handle(500, "UpdateLabel", err) + return + } + c.Redirect(c.Repo.RepoLink + "/labels") +} + +func DeleteLabel(c *context.Context) { + if err := models.DeleteLabel(c.Repo.Repository.ID, c.QueryInt64("id")); err != nil { + c.Flash.Error("DeleteLabel: " + err.Error()) + } else { + c.Flash.Success(c.Tr("repo.issues.label_deletion_success")) + } + + c.JSON(200, map[string]interface{}{ + "redirect": c.Repo.RepoLink + "/labels", + }) + return +} + +func Milestones(c *context.Context) { + c.Data["Title"] = c.Tr("repo.milestones") + c.Data["PageIsIssueList"] = true + c.Data["PageIsMilestones"] = true + + isShowClosed := c.Query("state") == "closed" + openCount, closedCount := models.MilestoneStats(c.Repo.Repository.ID) + c.Data["OpenCount"] = openCount + c.Data["ClosedCount"] = closedCount + + page := c.QueryInt("page") + if page <= 1 { + page = 1 + } + + var total int + if !isShowClosed { + total = int(openCount) + } else { + total = int(closedCount) + } + c.Data["Page"] = paginater.New(total, setting.UI.IssuePagingNum, page, 5) + + miles, err := models.GetMilestones(c.Repo.Repository.ID, page, isShowClosed) + if err != nil { + c.Handle(500, "GetMilestones", err) + return + } + for _, m := range miles { + m.NumOpenIssues = int(m.CountIssues(false, false)) + m.NumClosedIssues = int(m.CountIssues(true, false)) + if m.NumOpenIssues+m.NumClosedIssues > 0 { + m.Completeness = m.NumClosedIssues * 100 / (m.NumOpenIssues + m.NumClosedIssues) + } + m.RenderedContent = string(markup.Markdown(m.Content, c.Repo.RepoLink, c.Repo.Repository.ComposeMetas())) + } + c.Data["Milestones"] = miles + + if isShowClosed { + c.Data["State"] = "closed" + } else { + c.Data["State"] = "open" + } + + c.Data["IsShowClosed"] = isShowClosed + c.HTML(200, MILESTONE) +} + +func NewMilestone(c *context.Context) { + c.Data["Title"] = c.Tr("repo.milestones.new") + c.Data["PageIsIssueList"] = true + c.Data["PageIsMilestones"] = true + c.Data["RequireDatetimepicker"] = true + c.Data["DateLang"] = setting.DateLang(c.Locale.Language()) + c.HTML(200, MILESTONE_NEW) +} + +func NewMilestonePost(c *context.Context, f form.CreateMilestone) { + c.Data["Title"] = c.Tr("repo.milestones.new") + c.Data["PageIsIssueList"] = true + c.Data["PageIsMilestones"] = true + c.Data["RequireDatetimepicker"] = true + c.Data["DateLang"] = setting.DateLang(c.Locale.Language()) + + if c.HasError() { + c.HTML(200, MILESTONE_NEW) + return + } + + if len(f.Deadline) == 0 { + f.Deadline = "9999-12-31" + } + deadline, err := time.ParseInLocation("2006-01-02", f.Deadline, time.Local) + if err != nil { + c.Data["Err_Deadline"] = true + c.RenderWithErr(c.Tr("repo.milestones.invalid_due_date_format"), MILESTONE_NEW, &f) + return + } + + if err = models.NewMilestone(&models.Milestone{ + RepoID: c.Repo.Repository.ID, + Name: f.Title, + Content: f.Content, + Deadline: deadline, + }); err != nil { + c.Handle(500, "NewMilestone", err) + return + } + + c.Flash.Success(c.Tr("repo.milestones.create_success", f.Title)) + c.Redirect(c.Repo.RepoLink + "/milestones") +} + +func EditMilestone(c *context.Context) { + c.Data["Title"] = c.Tr("repo.milestones.edit") + c.Data["PageIsMilestones"] = true + c.Data["PageIsEditMilestone"] = true + c.Data["RequireDatetimepicker"] = true + c.Data["DateLang"] = setting.DateLang(c.Locale.Language()) + + m, err := models.GetMilestoneByRepoID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + if models.IsErrMilestoneNotExist(err) { + c.Handle(404, "", nil) + } else { + c.Handle(500, "GetMilestoneByRepoID", err) + } + return + } + c.Data["title"] = m.Name + c.Data["content"] = m.Content + if len(m.DeadlineString) > 0 { + c.Data["deadline"] = m.DeadlineString + } + c.HTML(200, MILESTONE_NEW) +} + +func EditMilestonePost(c *context.Context, f form.CreateMilestone) { + c.Data["Title"] = c.Tr("repo.milestones.edit") + c.Data["PageIsMilestones"] = true + c.Data["PageIsEditMilestone"] = true + c.Data["RequireDatetimepicker"] = true + c.Data["DateLang"] = setting.DateLang(c.Locale.Language()) + + if c.HasError() { + c.HTML(200, MILESTONE_NEW) + return + } + + if len(f.Deadline) == 0 { + f.Deadline = "9999-12-31" + } + deadline, err := time.ParseInLocation("2006-01-02", f.Deadline, time.Local) + if err != nil { + c.Data["Err_Deadline"] = true + c.RenderWithErr(c.Tr("repo.milestones.invalid_due_date_format"), MILESTONE_NEW, &f) + return + } + + m, err := models.GetMilestoneByRepoID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + if models.IsErrMilestoneNotExist(err) { + c.Handle(404, "", nil) + } else { + c.Handle(500, "GetMilestoneByRepoID", err) + } + return + } + m.Name = f.Title + m.Content = f.Content + m.Deadline = deadline + if err = models.UpdateMilestone(m); err != nil { + c.Handle(500, "UpdateMilestone", err) + return + } + + c.Flash.Success(c.Tr("repo.milestones.edit_success", m.Name)) + c.Redirect(c.Repo.RepoLink + "/milestones") +} + +func ChangeMilestonStatus(c *context.Context) { + m, err := models.GetMilestoneByRepoID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + if models.IsErrMilestoneNotExist(err) { + c.Handle(404, "", err) + } else { + c.Handle(500, "GetMilestoneByRepoID", err) + } + return + } + + switch c.Params(":action") { + case "open": + if m.IsClosed { + if err = models.ChangeMilestoneStatus(m, false); err != nil { + c.Handle(500, "ChangeMilestoneStatus", err) + return + } + } + c.Redirect(c.Repo.RepoLink + "/milestones?state=open") + case "close": + if !m.IsClosed { + m.ClosedDate = time.Now() + if err = models.ChangeMilestoneStatus(m, true); err != nil { + c.Handle(500, "ChangeMilestoneStatus", err) + return + } + } + c.Redirect(c.Repo.RepoLink + "/milestones?state=closed") + default: + c.Redirect(c.Repo.RepoLink + "/milestones") + } +} + +func DeleteMilestone(c *context.Context) { + if err := models.DeleteMilestoneOfRepoByID(c.Repo.Repository.ID, c.QueryInt64("id")); err != nil { + c.Flash.Error("DeleteMilestoneByRepoID: " + err.Error()) + } else { + c.Flash.Success(c.Tr("repo.milestones.deletion_success")) + } + + c.JSON(200, map[string]interface{}{ + "redirect": c.Repo.RepoLink + "/milestones", + }) +} diff --git a/routes/repo/pull.go b/routes/repo/pull.go new file mode 100644 index 00000000..73757280 --- /dev/null +++ b/routes/repo/pull.go @@ -0,0 +1,763 @@ +// 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 ( + "container/list" + "path" + "strings" + + "github.com/Unknwon/com" + log "gopkg.in/clog.v1" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/tool" +) + +const ( + FORK = "repo/pulls/fork" + COMPARE_PULL = "repo/pulls/compare" + PULL_COMMITS = "repo/pulls/commits" + PULL_FILES = "repo/pulls/files" + + PULL_REQUEST_TEMPLATE_KEY = "PullRequestTemplate" +) + +var ( + PullRequestTemplateCandidates = []string{ + "PULL_REQUEST.md", + ".gogs/PULL_REQUEST.md", + ".github/PULL_REQUEST.md", + } +) + +func parseBaseRepository(c *context.Context) *models.Repository { + baseRepo, err := models.GetRepositoryByID(c.ParamsInt64(":repoid")) + if err != nil { + c.NotFoundOrServerError("GetRepositoryByID", errors.IsRepoNotExist, err) + return nil + } + + if !baseRepo.CanBeForked() || !baseRepo.HasAccess(c.User.ID) { + c.NotFound() + return nil + } + + c.Data["repo_name"] = baseRepo.Name + c.Data["description"] = baseRepo.Description + c.Data["IsPrivate"] = baseRepo.IsPrivate + + if err = baseRepo.GetOwner(); err != nil { + c.ServerError("GetOwner", err) + return nil + } + c.Data["ForkFrom"] = baseRepo.Owner.Name + "/" + baseRepo.Name + + if err := c.User.GetOrganizations(true); err != nil { + c.ServerError("GetOrganizations", err) + return nil + } + c.Data["Orgs"] = c.User.Orgs + + return baseRepo +} + +func Fork(c *context.Context) { + c.Data["Title"] = c.Tr("new_fork") + + parseBaseRepository(c) + if c.Written() { + return + } + + c.Data["ContextUser"] = c.User + c.Success(FORK) +} + +func ForkPost(c *context.Context, f form.CreateRepo) { + c.Data["Title"] = c.Tr("new_fork") + + baseRepo := parseBaseRepository(c) + if c.Written() { + return + } + + ctxUser := checkContextUser(c, f.UserID) + if c.Written() { + return + } + c.Data["ContextUser"] = ctxUser + + if c.HasError() { + c.Success(FORK) + return + } + + repo, has := models.HasForkedRepo(ctxUser.ID, baseRepo.ID) + if has { + c.Redirect(repo.Link()) + return + } + + // Check ownership of organization. + if ctxUser.IsOrganization() && !ctxUser.IsOwnedBy(c.User.ID) { + c.Error(403) + return + } + + // Cannot fork to same owner + if ctxUser.ID == baseRepo.OwnerID { + c.RenderWithErr(c.Tr("repo.settings.cannot_fork_to_same_owner"), FORK, &f) + return + } + + repo, err := models.ForkRepository(c.User, ctxUser, baseRepo, f.RepoName, f.Description) + if err != nil { + c.Data["Err_RepoName"] = true + switch { + case models.IsErrRepoAlreadyExist(err): + c.RenderWithErr(c.Tr("repo.settings.new_owner_has_same_repo"), FORK, &f) + case models.IsErrNameReserved(err): + c.RenderWithErr(c.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), FORK, &f) + case models.IsErrNamePatternNotAllowed(err): + c.RenderWithErr(c.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), FORK, &f) + default: + c.ServerError("ForkPost", err) + } + return + } + + log.Trace("Repository forked from '%s' -> '%s'", baseRepo.FullName(), repo.FullName()) + c.Redirect(repo.Link()) +} + +func checkPullInfo(c *context.Context) *models.Issue { + issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, c.ParamsInt64(":index")) + if err != nil { + c.NotFoundOrServerError("GetIssueByIndex", errors.IsIssueNotExist, err) + return nil + } + c.Data["Title"] = issue.Title + c.Data["Issue"] = issue + + if !issue.IsPull { + c.Handle(404, "ViewPullCommits", nil) + return nil + } + + if c.IsLogged { + // Update issue-user. + if err = issue.ReadBy(c.User.ID); err != nil { + c.ServerError("ReadBy", err) + return nil + } + } + + return issue +} + +func PrepareMergedViewPullInfo(c *context.Context, issue *models.Issue) { + pull := issue.PullRequest + c.Data["HasMerged"] = true + c.Data["HeadTarget"] = issue.PullRequest.HeadUserName + "/" + pull.HeadBranch + c.Data["BaseTarget"] = c.Repo.Owner.Name + "/" + pull.BaseBranch + + var err error + c.Data["NumCommits"], err = c.Repo.GitRepo.CommitsCountBetween(pull.MergeBase, pull.MergedCommitID) + if err != nil { + c.ServerError("Repo.GitRepo.CommitsCountBetween", err) + return + } + c.Data["NumFiles"], err = c.Repo.GitRepo.FilesCountBetween(pull.MergeBase, pull.MergedCommitID) + if err != nil { + c.ServerError("Repo.GitRepo.FilesCountBetween", err) + return + } +} + +func PrepareViewPullInfo(c *context.Context, issue *models.Issue) *git.PullRequestInfo { + repo := c.Repo.Repository + pull := issue.PullRequest + + c.Data["HeadTarget"] = pull.HeadUserName + "/" + pull.HeadBranch + c.Data["BaseTarget"] = c.Repo.Owner.Name + "/" + pull.BaseBranch + + var ( + headGitRepo *git.Repository + err error + ) + + if pull.HeadRepo != nil { + headGitRepo, err = git.OpenRepository(pull.HeadRepo.RepoPath()) + if err != nil { + c.ServerError("OpenRepository", err) + return nil + } + } + + if pull.HeadRepo == nil || !headGitRepo.IsBranchExist(pull.HeadBranch) { + c.Data["IsPullReuqestBroken"] = true + c.Data["HeadTarget"] = "deleted" + c.Data["NumCommits"] = 0 + c.Data["NumFiles"] = 0 + return nil + } + + prInfo, err := headGitRepo.GetPullRequestInfo(models.RepoPath(repo.Owner.Name, repo.Name), + pull.BaseBranch, pull.HeadBranch) + if err != nil { + if strings.Contains(err.Error(), "fatal: Not a valid object name") { + c.Data["IsPullReuqestBroken"] = true + c.Data["BaseTarget"] = "deleted" + c.Data["NumCommits"] = 0 + c.Data["NumFiles"] = 0 + return nil + } + + c.ServerError("GetPullRequestInfo", err) + return nil + } + c.Data["NumCommits"] = prInfo.Commits.Len() + c.Data["NumFiles"] = prInfo.NumFiles + return prInfo +} + +func ViewPullCommits(c *context.Context) { + c.Data["PageIsPullList"] = true + c.Data["PageIsPullCommits"] = true + + issue := checkPullInfo(c) + if c.Written() { + return + } + pull := issue.PullRequest + + if pull.HeadRepo != nil { + c.Data["Username"] = pull.HeadUserName + c.Data["Reponame"] = pull.HeadRepo.Name + } + + var commits *list.List + if pull.HasMerged { + PrepareMergedViewPullInfo(c, issue) + if c.Written() { + return + } + startCommit, err := c.Repo.GitRepo.GetCommit(pull.MergeBase) + if err != nil { + c.ServerError("Repo.GitRepo.GetCommit", err) + return + } + endCommit, err := c.Repo.GitRepo.GetCommit(pull.MergedCommitID) + if err != nil { + c.ServerError("Repo.GitRepo.GetCommit", err) + return + } + commits, err = c.Repo.GitRepo.CommitsBetween(endCommit, startCommit) + if err != nil { + c.ServerError("Repo.GitRepo.CommitsBetween", err) + return + } + + } else { + prInfo := PrepareViewPullInfo(c, issue) + if c.Written() { + return + } else if prInfo == nil { + c.Handle(404, "ViewPullCommits", nil) + return + } + commits = prInfo.Commits + } + + commits = models.ValidateCommitsWithEmails(commits) + c.Data["Commits"] = commits + c.Data["CommitsCount"] = commits.Len() + + c.Success(PULL_COMMITS) +} + +func ViewPullFiles(c *context.Context) { + c.Data["PageIsPullList"] = true + c.Data["PageIsPullFiles"] = true + + issue := checkPullInfo(c) + if c.Written() { + return + } + pull := issue.PullRequest + + var ( + diffRepoPath string + startCommitID string + endCommitID string + gitRepo *git.Repository + ) + + if pull.HasMerged { + PrepareMergedViewPullInfo(c, issue) + if c.Written() { + return + } + + diffRepoPath = c.Repo.GitRepo.Path + startCommitID = pull.MergeBase + endCommitID = pull.MergedCommitID + gitRepo = c.Repo.GitRepo + } else { + prInfo := PrepareViewPullInfo(c, issue) + if c.Written() { + return + } else if prInfo == nil { + c.Handle(404, "ViewPullFiles", nil) + return + } + + headRepoPath := models.RepoPath(pull.HeadUserName, pull.HeadRepo.Name) + + headGitRepo, err := git.OpenRepository(headRepoPath) + if err != nil { + c.ServerError("OpenRepository", err) + return + } + + headCommitID, err := headGitRepo.GetBranchCommitID(pull.HeadBranch) + if err != nil { + c.ServerError("GetBranchCommitID", err) + return + } + + diffRepoPath = headRepoPath + startCommitID = prInfo.MergeBase + endCommitID = headCommitID + gitRepo = headGitRepo + } + + diff, err := models.GetDiffRange(diffRepoPath, + startCommitID, endCommitID, setting.Git.MaxGitDiffLines, + setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles) + if err != nil { + c.ServerError("GetDiffRange", err) + return + } + c.Data["Diff"] = diff + c.Data["DiffNotAvailable"] = diff.NumFiles() == 0 + + commit, err := gitRepo.GetCommit(endCommitID) + if err != nil { + c.ServerError("GetCommit", err) + return + } + + setEditorconfigIfExists(c) + if c.Written() { + return + } + + c.Data["IsSplitStyle"] = c.Query("style") == "split" + c.Data["IsImageFile"] = commit.IsImageFile + + // It is possible head repo has been deleted for merged pull requests + if pull.HeadRepo != nil { + c.Data["Username"] = pull.HeadUserName + c.Data["Reponame"] = pull.HeadRepo.Name + + headTarget := path.Join(pull.HeadUserName, pull.HeadRepo.Name) + c.Data["SourcePath"] = setting.AppSubURL + "/" + path.Join(headTarget, "src", endCommitID) + c.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(headTarget, "src", startCommitID) + c.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(headTarget, "raw", endCommitID) + } + + c.Data["RequireHighlightJS"] = true + c.Success(PULL_FILES) +} + +func MergePullRequest(c *context.Context) { + issue := checkPullInfo(c) + if c.Written() { + return + } + if issue.IsClosed { + c.Handle(404, "MergePullRequest", nil) + return + } + + pr, err := models.GetPullRequestByIssueID(issue.ID) + if err != nil { + c.NotFoundOrServerError("GetPullRequestByIssueID", models.IsErrPullRequestNotExist, err) + return + } + + if !pr.CanAutoMerge() || pr.HasMerged { + c.Handle(404, "MergePullRequest", nil) + return + } + + pr.Issue = issue + pr.Issue.Repo = c.Repo.Repository + if err = pr.Merge(c.User, c.Repo.GitRepo); err != nil { + c.ServerError("Merge", err) + return + } + + log.Trace("Pull request merged: %d", pr.ID) + c.Redirect(c.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index)) +} + +func ParseCompareInfo(c *context.Context) (*models.User, *models.Repository, *git.Repository, *git.PullRequestInfo, string, string) { + baseRepo := c.Repo.Repository + + // Get compared branches information + // format: <base branch>...[<head repo>:]<head branch> + // base<-head: master...head:feature + // same repo: master...feature + infos := strings.Split(c.Params("*"), "...") + if len(infos) != 2 { + log.Trace("ParseCompareInfo[%d]: not enough compared branches information %s", baseRepo.ID, infos) + c.NotFound() + return nil, nil, nil, nil, "", "" + } + + baseBranch := infos[0] + c.Data["BaseBranch"] = baseBranch + + var ( + headUser *models.User + headBranch string + isSameRepo bool + err error + ) + + // If there is no head repository, it means pull request between same repository. + headInfos := strings.Split(infos[1], ":") + if len(headInfos) == 1 { + isSameRepo = true + headUser = c.Repo.Owner + headBranch = headInfos[0] + + } else if len(headInfos) == 2 { + headUser, err = models.GetUserByName(headInfos[0]) + if err != nil { + c.NotFoundOrServerError("GetUserByName", errors.IsUserNotExist, err) + return nil, nil, nil, nil, "", "" + } + headBranch = headInfos[1] + isSameRepo = headUser.ID == baseRepo.OwnerID + + } else { + c.NotFound() + return nil, nil, nil, nil, "", "" + } + c.Data["HeadUser"] = headUser + c.Data["HeadBranch"] = headBranch + c.Repo.PullRequest.SameRepo = isSameRepo + + // Check if base branch is valid. + if !c.Repo.GitRepo.IsBranchExist(baseBranch) { + c.NotFound() + return nil, nil, nil, nil, "", "" + } + + var ( + headRepo *models.Repository + headGitRepo *git.Repository + ) + + // In case user included redundant head user name for comparison in same repository, + // no need to check the fork relation. + if !isSameRepo { + var has bool + headRepo, has = models.HasForkedRepo(headUser.ID, baseRepo.ID) + if !has { + log.Trace("ParseCompareInfo [base_repo_id: %d]: does not have fork or in same repository", baseRepo.ID) + c.NotFound() + return nil, nil, nil, nil, "", "" + } + + headGitRepo, err = git.OpenRepository(models.RepoPath(headUser.Name, headRepo.Name)) + if err != nil { + c.ServerError("OpenRepository", err) + return nil, nil, nil, nil, "", "" + } + } else { + headRepo = c.Repo.Repository + headGitRepo = c.Repo.GitRepo + } + + if !c.User.IsWriterOfRepo(headRepo) && !c.User.IsAdmin { + log.Trace("ParseCompareInfo [base_repo_id: %d]: does not have write access or site admin", baseRepo.ID) + c.NotFound() + return nil, nil, nil, nil, "", "" + } + + // Check if head branch is valid. + if !headGitRepo.IsBranchExist(headBranch) { + c.NotFound() + return nil, nil, nil, nil, "", "" + } + + headBranches, err := headGitRepo.GetBranches() + if err != nil { + c.ServerError("GetBranches", err) + return nil, nil, nil, nil, "", "" + } + c.Data["HeadBranches"] = headBranches + + prInfo, err := headGitRepo.GetPullRequestInfo(models.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch) + if err != nil { + if git.IsErrNoMergeBase(err) { + c.Data["IsNoMergeBase"] = true + c.Success(COMPARE_PULL) + } else { + c.ServerError("GetPullRequestInfo", err) + } + return nil, nil, nil, nil, "", "" + } + c.Data["BeforeCommitID"] = prInfo.MergeBase + + return headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch +} + +func PrepareCompareDiff( + c *context.Context, + headUser *models.User, + headRepo *models.Repository, + headGitRepo *git.Repository, + prInfo *git.PullRequestInfo, + baseBranch, headBranch string) bool { + + var ( + repo = c.Repo.Repository + err error + ) + + // Get diff information. + c.Data["CommitRepoLink"] = headRepo.Link() + + headCommitID, err := headGitRepo.GetBranchCommitID(headBranch) + if err != nil { + c.ServerError("GetBranchCommitID", err) + return false + } + c.Data["AfterCommitID"] = headCommitID + + if headCommitID == prInfo.MergeBase { + c.Data["IsNothingToCompare"] = true + return true + } + + diff, err := models.GetDiffRange(models.RepoPath(headUser.Name, headRepo.Name), + prInfo.MergeBase, headCommitID, setting.Git.MaxGitDiffLines, + setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles) + if err != nil { + c.ServerError("GetDiffRange", err) + return false + } + c.Data["Diff"] = diff + c.Data["DiffNotAvailable"] = diff.NumFiles() == 0 + + headCommit, err := headGitRepo.GetCommit(headCommitID) + if err != nil { + c.ServerError("GetCommit", err) + return false + } + + prInfo.Commits = models.ValidateCommitsWithEmails(prInfo.Commits) + c.Data["Commits"] = prInfo.Commits + c.Data["CommitCount"] = prInfo.Commits.Len() + c.Data["Username"] = headUser.Name + c.Data["Reponame"] = headRepo.Name + c.Data["IsImageFile"] = headCommit.IsImageFile + + headTarget := path.Join(headUser.Name, repo.Name) + c.Data["SourcePath"] = setting.AppSubURL + "/" + path.Join(headTarget, "src", headCommitID) + c.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(headTarget, "src", prInfo.MergeBase) + c.Data["RawPath"] = setting.AppSubURL + "/" + path.Join(headTarget, "raw", headCommitID) + return false +} + +func CompareAndPullRequest(c *context.Context) { + c.Data["Title"] = c.Tr("repo.pulls.compare_changes") + c.Data["PageIsComparePull"] = true + c.Data["IsDiffCompare"] = true + c.Data["RequireHighlightJS"] = true + setTemplateIfExists(c, PULL_REQUEST_TEMPLATE_KEY, PullRequestTemplateCandidates) + renderAttachmentSettings(c) + + headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch := ParseCompareInfo(c) + if c.Written() { + return + } + + pr, err := models.GetUnmergedPullRequest(headRepo.ID, c.Repo.Repository.ID, headBranch, baseBranch) + if err != nil { + if !models.IsErrPullRequestNotExist(err) { + c.ServerError("GetUnmergedPullRequest", err) + return + } + } else { + c.Data["HasPullRequest"] = true + c.Data["PullRequest"] = pr + c.Success(COMPARE_PULL) + return + } + + nothingToCompare := PrepareCompareDiff(c, headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch) + if c.Written() { + return + } + + if !nothingToCompare { + // Setup information for new form. + RetrieveRepoMetas(c, c.Repo.Repository) + if c.Written() { + return + } + } + + setEditorconfigIfExists(c) + if c.Written() { + return + } + + c.Data["IsSplitStyle"] = c.Query("style") == "split" + c.Success(COMPARE_PULL) +} + +func CompareAndPullRequestPost(c *context.Context, f form.NewIssue) { + c.Data["Title"] = c.Tr("repo.pulls.compare_changes") + c.Data["PageIsComparePull"] = true + c.Data["IsDiffCompare"] = true + c.Data["RequireHighlightJS"] = true + renderAttachmentSettings(c) + + var ( + repo = c.Repo.Repository + attachments []string + ) + + headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch := ParseCompareInfo(c) + if c.Written() { + return + } + + labelIDs, milestoneID, assigneeID := ValidateRepoMetas(c, f) + if c.Written() { + return + } + + if setting.AttachmentEnabled { + attachments = f.Files + } + + if c.HasError() { + form.Assign(f, c.Data) + + // This stage is already stop creating new pull request, so it does not matter if it has + // something to compare or not. + PrepareCompareDiff(c, headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch) + if c.Written() { + return + } + + c.Success(COMPARE_PULL) + return + } + + patch, err := headGitRepo.GetPatch(prInfo.MergeBase, headBranch) + if err != nil { + c.ServerError("GetPatch", err) + return + } + + pullIssue := &models.Issue{ + RepoID: repo.ID, + Index: repo.NextIssueIndex(), + Title: f.Title, + PosterID: c.User.ID, + Poster: c.User, + MilestoneID: milestoneID, + AssigneeID: assigneeID, + IsPull: true, + Content: f.Content, + } + pullRequest := &models.PullRequest{ + HeadRepoID: headRepo.ID, + BaseRepoID: repo.ID, + HeadUserName: headUser.Name, + HeadBranch: headBranch, + BaseBranch: baseBranch, + HeadRepo: headRepo, + BaseRepo: repo, + MergeBase: prInfo.MergeBase, + Type: models.PULL_REQUEST_GOGS, + } + // FIXME: check error in the case two people send pull request at almost same time, give nice error prompt + // instead of 500. + if err := models.NewPullRequest(repo, pullIssue, labelIDs, attachments, pullRequest, patch); err != nil { + c.ServerError("NewPullRequest", err) + return + } else if err := pullRequest.PushToBaseRepo(); err != nil { + c.ServerError("PushToBaseRepo", err) + return + } + + log.Trace("Pull request created: %d/%d", repo.ID, pullIssue.ID) + c.Redirect(c.Repo.RepoLink + "/pulls/" + com.ToStr(pullIssue.Index)) +} + +func parseOwnerAndRepo(c *context.Context) (*models.User, *models.Repository) { + owner, err := models.GetUserByName(c.Params(":username")) + if err != nil { + c.NotFoundOrServerError("GetUserByName", errors.IsUserNotExist, err) + return nil, nil + } + + repo, err := models.GetRepositoryByName(owner.ID, c.Params(":reponame")) + if err != nil { + c.NotFoundOrServerError("GetRepositoryByName", errors.IsRepoNotExist, err) + return nil, nil + } + + return owner, repo +} + +func TriggerTask(c *context.Context) { + pusherID := c.QueryInt64("pusher") + branch := c.Query("branch") + secret := c.Query("secret") + if len(branch) == 0 || len(secret) == 0 || pusherID <= 0 { + c.Error(404) + log.Trace("TriggerTask: branch or secret is empty, or pusher ID is not valid") + return + } + owner, repo := parseOwnerAndRepo(c) + if c.Written() { + return + } + if secret != tool.MD5(owner.Salt) { + c.Error(404) + log.Trace("TriggerTask [%s/%s]: invalid secret", owner.Name, repo.Name) + return + } + + pusher, err := models.GetUserByID(pusherID) + if err != nil { + c.NotFoundOrServerError("GetUserByID", errors.IsUserNotExist, err) + return + } + + log.Trace("TriggerTask '%s/%s' by '%s'", repo.Name, branch, pusher.Name) + + go models.HookQueue.Add(repo.ID) + go models.AddTestPullRequestTask(pusher, repo.ID, branch, true) + c.Status(202) +} diff --git a/routes/repo/release.go b/routes/repo/release.go new file mode 100644 index 00000000..86dfe6f7 --- /dev/null +++ b/routes/repo/release.go @@ -0,0 +1,332 @@ +// 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" + "strings" + + log "gopkg.in/clog.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/markup" + "github.com/gogits/gogs/pkg/setting" +) + +const ( + RELEASES = "repo/release/list" + RELEASE_NEW = "repo/release/new" +) + +// calReleaseNumCommitsBehind calculates given release has how many commits behind release target. +func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *models.Release, countCache map[string]int64) error { + // Get count if not exists + if _, ok := countCache[release.Target]; !ok { + if repoCtx.GitRepo.IsBranchExist(release.Target) { + commit, err := repoCtx.GitRepo.GetBranchCommit(release.Target) + if err != nil { + return fmt.Errorf("GetBranchCommit: %v", err) + } + countCache[release.Target], err = commit.CommitsCount() + if err != nil { + return fmt.Errorf("CommitsCount: %v", err) + } + } else { + // Use NumCommits of the newest release on that target + countCache[release.Target] = release.NumCommits + } + } + release.NumCommitsBehind = countCache[release.Target] - release.NumCommits + return nil +} + +func Releases(c *context.Context) { + c.Data["Title"] = c.Tr("repo.release.releases") + c.Data["PageIsViewFiles"] = true + c.Data["PageIsReleaseList"] = true + + tagsResult, err := c.Repo.GitRepo.GetTagsAfter(c.Query("after"), 10) + if err != nil { + c.Handle(500, fmt.Sprintf("GetTags '%s'", c.Repo.Repository.RepoPath()), err) + return + } + + releases, err := models.GetPublishedReleasesByRepoID(c.Repo.Repository.ID, tagsResult.Tags...) + if err != nil { + c.Handle(500, "GetPublishedReleasesByRepoID", err) + return + } + + // Temproray cache commits count of used branches to speed up. + countCache := make(map[string]int64) + + results := make([]*models.Release, len(tagsResult.Tags)) + for i, rawTag := range tagsResult.Tags { + for j, r := range releases { + if r == nil || r.TagName != rawTag { + continue + } + releases[j] = nil // Mark as used. + + if err = r.LoadAttributes(); err != nil { + c.Handle(500, "LoadAttributes", err) + return + } + + if err := calReleaseNumCommitsBehind(c.Repo, r, countCache); err != nil { + c.Handle(500, "calReleaseNumCommitsBehind", err) + return + } + + r.Note = string(markup.Markdown(r.Note, c.Repo.RepoLink, c.Repo.Repository.ComposeMetas())) + results[i] = r + break + } + + // No published release matches this tag + if results[i] == nil { + commit, err := c.Repo.GitRepo.GetTagCommit(rawTag) + if err != nil { + c.Handle(500, "GetTagCommit", err) + return + } + + results[i] = &models.Release{ + Title: rawTag, + TagName: rawTag, + Sha1: commit.ID.String(), + } + + results[i].NumCommits, err = commit.CommitsCount() + if err != nil { + c.Handle(500, "CommitsCount", err) + return + } + results[i].NumCommitsBehind = c.Repo.CommitsCount - results[i].NumCommits + } + } + models.SortReleases(results) + + // Only show drafts if user is viewing the latest page + var drafts []*models.Release + if tagsResult.HasLatest { + drafts, err = models.GetDraftReleasesByRepoID(c.Repo.Repository.ID) + if err != nil { + c.Handle(500, "GetDraftReleasesByRepoID", err) + return + } + + for _, r := range drafts { + if err = r.LoadAttributes(); err != nil { + c.Handle(500, "LoadAttributes", err) + return + } + + if err := calReleaseNumCommitsBehind(c.Repo, r, countCache); err != nil { + c.Handle(500, "calReleaseNumCommitsBehind", err) + return + } + + r.Note = string(markup.Markdown(r.Note, c.Repo.RepoLink, c.Repo.Repository.ComposeMetas())) + } + + if len(drafts) > 0 { + results = append(drafts, results...) + } + } + + c.Data["Releases"] = results + c.Data["HasPrevious"] = !tagsResult.HasLatest + c.Data["ReachEnd"] = tagsResult.ReachEnd + c.Data["PreviousAfter"] = tagsResult.PreviousAfter + if len(results) > 0 { + c.Data["NextAfter"] = results[len(results)-1].TagName + } + c.HTML(200, RELEASES) +} + +func renderReleaseAttachmentSettings(c *context.Context) { + c.Data["RequireDropzone"] = true + c.Data["IsAttachmentEnabled"] = setting.Release.Attachment.Enabled + c.Data["AttachmentAllowedTypes"] = strings.Join(setting.Release.Attachment.AllowedTypes, ",") + c.Data["AttachmentMaxSize"] = setting.Release.Attachment.MaxSize + c.Data["AttachmentMaxFiles"] = setting.Release.Attachment.MaxFiles +} + +func NewRelease(c *context.Context) { + c.Data["Title"] = c.Tr("repo.release.new_release") + c.Data["PageIsReleaseList"] = true + c.Data["tag_target"] = c.Repo.Repository.DefaultBranch + renderReleaseAttachmentSettings(c) + c.HTML(200, RELEASE_NEW) +} + +func NewReleasePost(c *context.Context, f form.NewRelease) { + c.Data["Title"] = c.Tr("repo.release.new_release") + c.Data["PageIsReleaseList"] = true + renderReleaseAttachmentSettings(c) + + if c.HasError() { + c.HTML(200, RELEASE_NEW) + return + } + + if !c.Repo.GitRepo.IsBranchExist(f.Target) { + c.RenderWithErr(c.Tr("form.target_branch_not_exist"), RELEASE_NEW, &f) + return + } + + // Use current time if tag not yet exist, otherwise get time from Git + var tagCreatedUnix int64 + tag, err := c.Repo.GitRepo.GetTag(f.TagName) + if err == nil { + commit, err := tag.Commit() + if err == nil { + tagCreatedUnix = commit.Author.When.Unix() + } + } + + commit, err := c.Repo.GitRepo.GetBranchCommit(f.Target) + if err != nil { + c.Handle(500, "GetBranchCommit", err) + return + } + + commitsCount, err := commit.CommitsCount() + if err != nil { + c.Handle(500, "CommitsCount", err) + return + } + + var attachments []string + if setting.Release.Attachment.Enabled { + attachments = f.Files + } + + rel := &models.Release{ + RepoID: c.Repo.Repository.ID, + PublisherID: c.User.ID, + Title: f.Title, + TagName: f.TagName, + Target: f.Target, + Sha1: commit.ID.String(), + NumCommits: commitsCount, + Note: f.Content, + IsDraft: len(f.Draft) > 0, + IsPrerelease: f.Prerelease, + CreatedUnix: tagCreatedUnix, + } + if err = models.NewRelease(c.Repo.GitRepo, rel, attachments); err != nil { + c.Data["Err_TagName"] = true + switch { + case models.IsErrReleaseAlreadyExist(err): + c.RenderWithErr(c.Tr("repo.release.tag_name_already_exist"), RELEASE_NEW, &f) + case models.IsErrInvalidTagName(err): + c.RenderWithErr(c.Tr("repo.release.tag_name_invalid"), RELEASE_NEW, &f) + default: + c.Handle(500, "NewRelease", err) + } + return + } + log.Trace("Release created: %s/%s:%s", c.User.LowerName, c.Repo.Repository.Name, f.TagName) + + c.Redirect(c.Repo.RepoLink + "/releases") +} + +func EditRelease(c *context.Context) { + c.Data["Title"] = c.Tr("repo.release.edit_release") + c.Data["PageIsReleaseList"] = true + c.Data["PageIsEditRelease"] = true + renderReleaseAttachmentSettings(c) + + tagName := c.Params("*") + rel, err := models.GetRelease(c.Repo.Repository.ID, tagName) + if err != nil { + if models.IsErrReleaseNotExist(err) { + c.Handle(404, "GetRelease", err) + } else { + c.Handle(500, "GetRelease", err) + } + return + } + c.Data["ID"] = rel.ID + c.Data["tag_name"] = rel.TagName + c.Data["tag_target"] = rel.Target + c.Data["title"] = rel.Title + c.Data["content"] = rel.Note + c.Data["attachments"] = rel.Attachments + c.Data["prerelease"] = rel.IsPrerelease + c.Data["IsDraft"] = rel.IsDraft + + c.HTML(200, RELEASE_NEW) +} + +func EditReleasePost(c *context.Context, f form.EditRelease) { + c.Data["Title"] = c.Tr("repo.release.edit_release") + c.Data["PageIsReleaseList"] = true + c.Data["PageIsEditRelease"] = true + renderReleaseAttachmentSettings(c) + + tagName := c.Params("*") + rel, err := models.GetRelease(c.Repo.Repository.ID, tagName) + if err != nil { + if models.IsErrReleaseNotExist(err) { + c.Handle(404, "GetRelease", err) + } else { + c.Handle(500, "GetRelease", err) + } + return + } + c.Data["tag_name"] = rel.TagName + c.Data["tag_target"] = rel.Target + c.Data["title"] = rel.Title + c.Data["content"] = rel.Note + c.Data["attachments"] = rel.Attachments + c.Data["prerelease"] = rel.IsPrerelease + c.Data["IsDraft"] = rel.IsDraft + + if c.HasError() { + c.HTML(200, RELEASE_NEW) + return + } + + var attachments []string + if setting.Release.Attachment.Enabled { + attachments = f.Files + } + + isPublish := rel.IsDraft && len(f.Draft) == 0 + rel.Title = f.Title + rel.Note = f.Content + rel.IsDraft = len(f.Draft) > 0 + rel.IsPrerelease = f.Prerelease + if err = models.UpdateRelease(c.User, c.Repo.GitRepo, rel, isPublish, attachments); err != nil { + c.Handle(500, "UpdateRelease", err) + return + } + c.Redirect(c.Repo.RepoLink + "/releases") +} + +func UploadReleaseAttachment(c *context.Context) { + if !setting.Release.Attachment.Enabled { + c.NotFound() + return + } + uploadAttachment(c, setting.Release.Attachment.AllowedTypes) +} + +func DeleteRelease(c *context.Context) { + if err := models.DeleteReleaseOfRepoByID(c.Repo.Repository.ID, c.QueryInt64("id")); err != nil { + c.Flash.Error("DeleteReleaseByID: " + err.Error()) + } else { + c.Flash.Success(c.Tr("repo.release.deletion_success")) + } + + c.JSON(200, map[string]interface{}{ + "redirect": c.Repo.RepoLink + "/releases", + }) +} diff --git a/routes/repo/repo.go b/routes/repo/repo.go new file mode 100644 index 00000000..ea3c1a60 --- /dev/null +++ b/routes/repo/repo.go @@ -0,0 +1,335 @@ +// 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" + "os" + "path" + "strings" + + "github.com/Unknwon/com" + log "gopkg.in/clog.v1" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/tool" +) + +const ( + CREATE = "repo/create" + MIGRATE = "repo/migrate" +) + +func MustBeNotBare(c *context.Context) { + if c.Repo.Repository.IsBare { + c.Handle(404, "MustBeNotBare", nil) + } +} + +func checkContextUser(c *context.Context, uid int64) *models.User { + orgs, err := models.GetOwnedOrgsByUserIDDesc(c.User.ID, "updated_unix") + if err != nil { + c.Handle(500, "GetOwnedOrgsByUserIDDesc", err) + return nil + } + c.Data["Orgs"] = orgs + + // Not equal means current user is an organization. + if uid == c.User.ID || uid == 0 { + return c.User + } + + org, err := models.GetUserByID(uid) + if errors.IsUserNotExist(err) { + return c.User + } + + if err != nil { + c.Handle(500, "GetUserByID", fmt.Errorf("[%d]: %v", uid, err)) + return nil + } + + // Check ownership of organization. + if !org.IsOrganization() || !(c.User.IsAdmin || org.IsOwnedBy(c.User.ID)) { + c.Error(403) + return nil + } + return org +} + +func Create(c *context.Context) { + c.Data["Title"] = c.Tr("new_repo") + + // Give default value for template to render. + c.Data["Gitignores"] = models.Gitignores + c.Data["Licenses"] = models.Licenses + c.Data["Readmes"] = models.Readmes + c.Data["readme"] = "Default" + c.Data["private"] = c.User.LastRepoVisibility + c.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate + + ctxUser := checkContextUser(c, c.QueryInt64("org")) + if c.Written() { + return + } + c.Data["ContextUser"] = ctxUser + + c.HTML(200, CREATE) +} + +func handleCreateError(c *context.Context, owner *models.User, err error, name, tpl string, form interface{}) { + switch { + case errors.IsReachLimitOfRepo(err): + c.RenderWithErr(c.Tr("repo.form.reach_limit_of_creation", owner.RepoCreationNum()), tpl, form) + case models.IsErrRepoAlreadyExist(err): + c.Data["Err_RepoName"] = true + c.RenderWithErr(c.Tr("form.repo_name_been_taken"), tpl, form) + case models.IsErrNameReserved(err): + c.Data["Err_RepoName"] = true + c.RenderWithErr(c.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form) + case models.IsErrNamePatternNotAllowed(err): + c.Data["Err_RepoName"] = true + c.RenderWithErr(c.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form) + default: + c.Handle(500, name, err) + } +} + +func CreatePost(c *context.Context, f form.CreateRepo) { + c.Data["Title"] = c.Tr("new_repo") + + c.Data["Gitignores"] = models.Gitignores + c.Data["Licenses"] = models.Licenses + c.Data["Readmes"] = models.Readmes + + ctxUser := checkContextUser(c, f.UserID) + if c.Written() { + return + } + c.Data["ContextUser"] = ctxUser + + if c.HasError() { + c.HTML(200, CREATE) + return + } + + repo, err := models.CreateRepository(c.User, ctxUser, models.CreateRepoOptions{ + Name: f.RepoName, + Description: f.Description, + Gitignores: f.Gitignores, + License: f.License, + Readme: f.Readme, + IsPrivate: f.Private || setting.Repository.ForcePrivate, + AutoInit: f.AutoInit, + }) + if err == nil { + log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) + c.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + repo.Name) + return + } + + if repo != nil { + if errDelete := models.DeleteRepository(ctxUser.ID, repo.ID); errDelete != nil { + log.Error(4, "DeleteRepository: %v", errDelete) + } + } + + handleCreateError(c, ctxUser, err, "CreatePost", CREATE, &f) +} + +func Migrate(c *context.Context) { + c.Data["Title"] = c.Tr("new_migrate") + c.Data["private"] = c.User.LastRepoVisibility + c.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate + c.Data["mirror"] = c.Query("mirror") == "1" + + ctxUser := checkContextUser(c, c.QueryInt64("org")) + if c.Written() { + return + } + c.Data["ContextUser"] = ctxUser + + c.HTML(200, MIGRATE) +} + +func MigratePost(c *context.Context, f form.MigrateRepo) { + c.Data["Title"] = c.Tr("new_migrate") + + ctxUser := checkContextUser(c, f.Uid) + if c.Written() { + return + } + c.Data["ContextUser"] = ctxUser + + if c.HasError() { + c.HTML(200, MIGRATE) + return + } + + remoteAddr, err := f.ParseRemoteAddr(c.User) + if err != nil { + if models.IsErrInvalidCloneAddr(err) { + c.Data["Err_CloneAddr"] = true + addrErr := err.(models.ErrInvalidCloneAddr) + switch { + case addrErr.IsURLError: + c.RenderWithErr(c.Tr("form.url_error"), MIGRATE, &f) + case addrErr.IsPermissionDenied: + c.RenderWithErr(c.Tr("repo.migrate.permission_denied"), MIGRATE, &f) + case addrErr.IsInvalidPath: + c.RenderWithErr(c.Tr("repo.migrate.invalid_local_path"), MIGRATE, &f) + default: + c.Handle(500, "Unknown error", err) + } + } else { + c.Handle(500, "ParseRemoteAddr", err) + } + return + } + + repo, err := models.MigrateRepository(c.User, ctxUser, models.MigrateRepoOptions{ + Name: f.RepoName, + Description: f.Description, + IsPrivate: f.Private || setting.Repository.ForcePrivate, + IsMirror: f.Mirror, + RemoteAddr: remoteAddr, + }) + if err == nil { + log.Trace("Repository migrated [%d]: %s/%s", repo.ID, ctxUser.Name, f.RepoName) + c.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + f.RepoName) + return + } + + if repo != nil { + if errDelete := models.DeleteRepository(ctxUser.ID, repo.ID); errDelete != nil { + log.Error(4, "DeleteRepository: %v", errDelete) + } + } + + if strings.Contains(err.Error(), "Authentication failed") || + strings.Contains(err.Error(), "could not read Username") { + c.Data["Err_Auth"] = true + c.RenderWithErr(c.Tr("form.auth_failed", models.HandleMirrorCredentials(err.Error(), true)), MIGRATE, &f) + return + } else if strings.Contains(err.Error(), "fatal:") { + c.Data["Err_CloneAddr"] = true + c.RenderWithErr(c.Tr("repo.migrate.failed", models.HandleMirrorCredentials(err.Error(), true)), MIGRATE, &f) + return + } + + handleCreateError(c, ctxUser, err, "MigratePost", MIGRATE, &f) +} + +func Action(c *context.Context) { + var err error + switch c.Params(":action") { + case "watch": + err = models.WatchRepo(c.User.ID, c.Repo.Repository.ID, true) + case "unwatch": + err = models.WatchRepo(c.User.ID, c.Repo.Repository.ID, false) + case "star": + err = models.StarRepo(c.User.ID, c.Repo.Repository.ID, true) + case "unstar": + err = models.StarRepo(c.User.ID, c.Repo.Repository.ID, false) + case "desc": // FIXME: this is not used + if !c.Repo.IsOwner() { + c.Error(404) + return + } + + c.Repo.Repository.Description = c.Query("desc") + c.Repo.Repository.Website = c.Query("site") + err = models.UpdateRepository(c.Repo.Repository, false) + } + + if err != nil { + c.Handle(500, fmt.Sprintf("Action (%s)", c.Params(":action")), err) + return + } + + redirectTo := c.Query("redirect_to") + if len(redirectTo) == 0 { + redirectTo = c.Repo.RepoLink + } + c.Redirect(redirectTo) +} + +func Download(c *context.Context) { + var ( + uri = c.Params("*") + refName string + ext string + archivePath string + archiveType git.ArchiveType + ) + + switch { + case strings.HasSuffix(uri, ".zip"): + ext = ".zip" + archivePath = path.Join(c.Repo.GitRepo.Path, "archives/zip") + archiveType = git.ZIP + case strings.HasSuffix(uri, ".tar.gz"): + ext = ".tar.gz" + archivePath = path.Join(c.Repo.GitRepo.Path, "archives/targz") + archiveType = git.TARGZ + default: + log.Trace("Unknown format: %s", uri) + c.Error(404) + return + } + refName = strings.TrimSuffix(uri, ext) + + if !com.IsDir(archivePath) { + if err := os.MkdirAll(archivePath, os.ModePerm); err != nil { + c.Handle(500, "Download -> os.MkdirAll(archivePath)", err) + return + } + } + + // Get corresponding commit. + var ( + commit *git.Commit + err error + ) + gitRepo := c.Repo.GitRepo + if gitRepo.IsBranchExist(refName) { + commit, err = gitRepo.GetBranchCommit(refName) + if err != nil { + c.Handle(500, "GetBranchCommit", err) + return + } + } else if gitRepo.IsTagExist(refName) { + commit, err = gitRepo.GetTagCommit(refName) + if err != nil { + c.Handle(500, "GetTagCommit", err) + return + } + } else if len(refName) >= 7 && len(refName) <= 40 { + commit, err = gitRepo.GetCommit(refName) + if err != nil { + c.NotFound() + return + } + } else { + c.NotFound() + return + } + + archivePath = path.Join(archivePath, tool.ShortSHA1(commit.ID.String())+ext) + if !com.IsFile(archivePath) { + if err := commit.CreateArchive(archivePath, archiveType); err != nil { + c.Handle(500, "Download -> CreateArchive "+archivePath, err) + return + } + } + + c.ServeFile(archivePath, c.Repo.Repository.Name+"-"+refName+ext) +} diff --git a/routes/repo/setting.go b/routes/repo/setting.go new file mode 100644 index 00000000..9168b04a --- /dev/null +++ b/routes/repo/setting.go @@ -0,0 +1,631 @@ +// 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" + "strings" + "time" + + log "gopkg.in/clog.v1" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/mailer" + "github.com/gogits/gogs/pkg/setting" +) + +const ( + SETTINGS_OPTIONS = "repo/settings/options" + SETTINGS_COLLABORATION = "repo/settings/collaboration" + SETTINGS_BRANCHES = "repo/settings/branches" + SETTINGS_PROTECTED_BRANCH = "repo/settings/protected_branch" + SETTINGS_GITHOOKS = "repo/settings/githooks" + SETTINGS_GITHOOK_EDIT = "repo/settings/githook_edit" + SETTINGS_DEPLOY_KEYS = "repo/settings/deploy_keys" +) + +func Settings(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings") + c.Data["PageIsSettingsOptions"] = true + c.HTML(200, SETTINGS_OPTIONS) +} + +func SettingsPost(c *context.Context, f form.RepoSetting) { + c.Data["Title"] = c.Tr("repo.settings") + c.Data["PageIsSettingsOptions"] = true + + repo := c.Repo.Repository + + switch c.Query("action") { + case "update": + if c.HasError() { + c.HTML(200, SETTINGS_OPTIONS) + return + } + + isNameChanged := false + oldRepoName := repo.Name + newRepoName := f.RepoName + // Check if repository name has been changed. + if repo.LowerName != strings.ToLower(newRepoName) { + isNameChanged = true + if err := models.ChangeRepositoryName(c.Repo.Owner, repo.Name, newRepoName); err != nil { + c.Data["Err_RepoName"] = true + switch { + case models.IsErrRepoAlreadyExist(err): + c.RenderWithErr(c.Tr("form.repo_name_been_taken"), SETTINGS_OPTIONS, &f) + case models.IsErrNameReserved(err): + c.RenderWithErr(c.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), SETTINGS_OPTIONS, &f) + case models.IsErrNamePatternNotAllowed(err): + c.RenderWithErr(c.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), SETTINGS_OPTIONS, &f) + default: + c.Handle(500, "ChangeRepositoryName", err) + } + return + } + + log.Trace("Repository name changed: %s/%s -> %s", c.Repo.Owner.Name, repo.Name, newRepoName) + } + // In case it's just a case change. + repo.Name = newRepoName + repo.LowerName = strings.ToLower(newRepoName) + + repo.Description = f.Description + repo.Website = f.Website + + // Visibility of forked repository is forced sync with base repository. + if repo.IsFork { + f.Private = repo.BaseRepo.IsPrivate + } + + visibilityChanged := repo.IsPrivate != f.Private + repo.IsPrivate = f.Private + if err := models.UpdateRepository(repo, visibilityChanged); err != nil { + c.Handle(500, "UpdateRepository", err) + return + } + log.Trace("Repository basic settings updated: %s/%s", c.Repo.Owner.Name, repo.Name) + + if isNameChanged { + if err := models.RenameRepoAction(c.User, oldRepoName, repo); err != nil { + log.Error(4, "RenameRepoAction: %v", err) + } + } + + c.Flash.Success(c.Tr("repo.settings.update_settings_success")) + c.Redirect(repo.Link() + "/settings") + + case "mirror": + if !repo.IsMirror { + c.Handle(404, "", nil) + return + } + + if f.Interval > 0 { + c.Repo.Mirror.EnablePrune = f.EnablePrune + c.Repo.Mirror.Interval = f.Interval + c.Repo.Mirror.NextUpdate = time.Now().Add(time.Duration(f.Interval) * time.Hour) + if err := models.UpdateMirror(c.Repo.Mirror); err != nil { + c.Handle(500, "UpdateMirror", err) + return + } + } + if err := c.Repo.Mirror.SaveAddress(f.MirrorAddress); err != nil { + c.Handle(500, "SaveAddress", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.update_settings_success")) + c.Redirect(repo.Link() + "/settings") + + case "mirror-sync": + if !repo.IsMirror { + c.Handle(404, "", nil) + return + } + + go models.MirrorQueue.Add(repo.ID) + c.Flash.Info(c.Tr("repo.settings.mirror_sync_in_progress")) + c.Redirect(repo.Link() + "/settings") + + case "advanced": + repo.EnableWiki = f.EnableWiki + repo.AllowPublicWiki = f.AllowPublicWiki + repo.EnableExternalWiki = f.EnableExternalWiki + repo.ExternalWikiURL = f.ExternalWikiURL + repo.EnableIssues = f.EnableIssues + repo.AllowPublicIssues = f.AllowPublicIssues + repo.EnableExternalTracker = f.EnableExternalTracker + repo.ExternalTrackerURL = f.ExternalTrackerURL + repo.ExternalTrackerFormat = f.TrackerURLFormat + repo.ExternalTrackerStyle = f.TrackerIssueStyle + repo.EnablePulls = f.EnablePulls + + if err := models.UpdateRepository(repo, false); err != nil { + c.Handle(500, "UpdateRepository", err) + return + } + log.Trace("Repository advanced settings updated: %s/%s", c.Repo.Owner.Name, repo.Name) + + c.Flash.Success(c.Tr("repo.settings.update_settings_success")) + c.Redirect(c.Repo.RepoLink + "/settings") + + case "convert": + if !c.Repo.IsOwner() { + c.Error(404) + return + } + if repo.Name != f.RepoName { + c.RenderWithErr(c.Tr("form.enterred_invalid_repo_name"), SETTINGS_OPTIONS, nil) + return + } + + if c.Repo.Owner.IsOrganization() { + if !c.Repo.Owner.IsOwnedBy(c.User.ID) { + c.Error(404) + return + } + } + + if !repo.IsMirror { + c.Error(404) + return + } + repo.IsMirror = false + + if _, err := models.CleanUpMigrateInfo(repo); err != nil { + c.Handle(500, "CleanUpMigrateInfo", err) + return + } else if err = models.DeleteMirrorByRepoID(c.Repo.Repository.ID); err != nil { + c.Handle(500, "DeleteMirrorByRepoID", err) + return + } + log.Trace("Repository converted from mirror to regular: %s/%s", c.Repo.Owner.Name, repo.Name) + c.Flash.Success(c.Tr("repo.settings.convert_succeed")) + c.Redirect(setting.AppSubURL + "/" + c.Repo.Owner.Name + "/" + repo.Name) + + case "transfer": + if !c.Repo.IsOwner() { + c.Error(404) + return + } + if repo.Name != f.RepoName { + c.RenderWithErr(c.Tr("form.enterred_invalid_repo_name"), SETTINGS_OPTIONS, nil) + return + } + + if c.Repo.Owner.IsOrganization() && !c.User.IsAdmin { + if !c.Repo.Owner.IsOwnedBy(c.User.ID) { + c.Error(404) + return + } + } + + newOwner := c.Query("new_owner_name") + isExist, err := models.IsUserExist(0, newOwner) + if err != nil { + c.Handle(500, "IsUserExist", err) + return + } else if !isExist { + c.RenderWithErr(c.Tr("form.enterred_invalid_owner_name"), SETTINGS_OPTIONS, nil) + return + } + + if err = models.TransferOwnership(c.User, newOwner, repo); err != nil { + if models.IsErrRepoAlreadyExist(err) { + c.RenderWithErr(c.Tr("repo.settings.new_owner_has_same_repo"), SETTINGS_OPTIONS, nil) + } else { + c.Handle(500, "TransferOwnership", err) + } + return + } + log.Trace("Repository transfered: %s/%s -> %s", c.Repo.Owner.Name, repo.Name, newOwner) + c.Flash.Success(c.Tr("repo.settings.transfer_succeed")) + c.Redirect(setting.AppSubURL + "/" + newOwner + "/" + repo.Name) + + case "delete": + if !c.Repo.IsOwner() { + c.Error(404) + return + } + if repo.Name != f.RepoName { + c.RenderWithErr(c.Tr("form.enterred_invalid_repo_name"), SETTINGS_OPTIONS, nil) + return + } + + if c.Repo.Owner.IsOrganization() && !c.User.IsAdmin { + if !c.Repo.Owner.IsOwnedBy(c.User.ID) { + c.Error(404) + return + } + } + + if err := models.DeleteRepository(c.Repo.Owner.ID, repo.ID); err != nil { + c.Handle(500, "DeleteRepository", err) + return + } + log.Trace("Repository deleted: %s/%s", c.Repo.Owner.Name, repo.Name) + + c.Flash.Success(c.Tr("repo.settings.deletion_success")) + c.Redirect(c.Repo.Owner.DashboardLink()) + + case "delete-wiki": + if !c.Repo.IsOwner() { + c.Error(404) + return + } + if repo.Name != f.RepoName { + c.RenderWithErr(c.Tr("form.enterred_invalid_repo_name"), SETTINGS_OPTIONS, nil) + return + } + + if c.Repo.Owner.IsOrganization() && !c.User.IsAdmin { + if !c.Repo.Owner.IsOwnedBy(c.User.ID) { + c.Error(404) + return + } + } + + repo.DeleteWiki() + log.Trace("Repository wiki deleted: %s/%s", c.Repo.Owner.Name, repo.Name) + + repo.EnableWiki = false + if err := models.UpdateRepository(repo, false); err != nil { + c.Handle(500, "UpdateRepository", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.wiki_deletion_success")) + c.Redirect(c.Repo.RepoLink + "/settings") + + default: + c.Handle(404, "", nil) + } +} + +func SettingsCollaboration(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings") + c.Data["PageIsSettingsCollaboration"] = true + + users, err := c.Repo.Repository.GetCollaborators() + if err != nil { + c.Handle(500, "GetCollaborators", err) + return + } + c.Data["Collaborators"] = users + + c.HTML(200, SETTINGS_COLLABORATION) +} + +func SettingsCollaborationPost(c *context.Context) { + name := strings.ToLower(c.Query("collaborator")) + if len(name) == 0 || c.Repo.Owner.LowerName == name { + c.Redirect(setting.AppSubURL + c.Req.URL.Path) + return + } + + u, err := models.GetUserByName(name) + if err != nil { + if errors.IsUserNotExist(err) { + c.Flash.Error(c.Tr("form.user_not_exist")) + c.Redirect(setting.AppSubURL + c.Req.URL.Path) + } else { + c.Handle(500, "GetUserByName", err) + } + return + } + + // Organization is not allowed to be added as a collaborator + if u.IsOrganization() { + c.Flash.Error(c.Tr("repo.settings.org_not_allowed_to_be_collaborator")) + c.Redirect(setting.AppSubURL + c.Req.URL.Path) + return + } + + if err = c.Repo.Repository.AddCollaborator(u); err != nil { + c.Handle(500, "AddCollaborator", err) + return + } + + if setting.Service.EnableNotifyMail { + mailer.SendCollaboratorMail(models.NewMailerUser(u), models.NewMailerUser(c.User), models.NewMailerRepo(c.Repo.Repository)) + } + + c.Flash.Success(c.Tr("repo.settings.add_collaborator_success")) + c.Redirect(setting.AppSubURL + c.Req.URL.Path) +} + +func ChangeCollaborationAccessMode(c *context.Context) { + if err := c.Repo.Repository.ChangeCollaborationAccessMode( + c.QueryInt64("uid"), + models.AccessMode(c.QueryInt("mode"))); err != nil { + log.Error(2, "ChangeCollaborationAccessMode: %v", err) + return + } + + c.Status(204) +} + +func DeleteCollaboration(c *context.Context) { + if err := c.Repo.Repository.DeleteCollaboration(c.QueryInt64("id")); err != nil { + c.Flash.Error("DeleteCollaboration: " + err.Error()) + } else { + c.Flash.Success(c.Tr("repo.settings.remove_collaborator_success")) + } + + c.JSON(200, map[string]interface{}{ + "redirect": c.Repo.RepoLink + "/settings/collaboration", + }) +} + +func SettingsBranches(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings.branches") + c.Data["PageIsSettingsBranches"] = true + + if c.Repo.Repository.IsBare { + c.Flash.Info(c.Tr("repo.settings.branches_bare"), true) + c.HTML(200, SETTINGS_BRANCHES) + return + } + + protectBranches, err := models.GetProtectBranchesByRepoID(c.Repo.Repository.ID) + if err != nil { + c.Handle(500, "GetProtectBranchesByRepoID", err) + return + } + + // Filter out deleted branches + branches := make([]string, 0, len(protectBranches)) + for i := range protectBranches { + if c.Repo.GitRepo.IsBranchExist(protectBranches[i].Name) { + branches = append(branches, protectBranches[i].Name) + } + } + c.Data["ProtectBranches"] = branches + + c.HTML(200, SETTINGS_BRANCHES) +} + +func UpdateDefaultBranch(c *context.Context) { + branch := c.Query("branch") + if c.Repo.GitRepo.IsBranchExist(branch) && + c.Repo.Repository.DefaultBranch != branch { + c.Repo.Repository.DefaultBranch = branch + if err := c.Repo.GitRepo.SetDefaultBranch(branch); err != nil { + if !git.IsErrUnsupportedVersion(err) { + c.Handle(500, "SetDefaultBranch", err) + return + } + + c.Flash.Warning(c.Tr("repo.settings.update_default_branch_unsupported")) + c.Redirect(c.Repo.RepoLink + "/settings/branches") + return + } + } + + if err := models.UpdateRepository(c.Repo.Repository, false); err != nil { + c.Handle(500, "UpdateRepository", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.update_default_branch_success")) + c.Redirect(c.Repo.RepoLink + "/settings/branches") +} + +func SettingsProtectedBranch(c *context.Context) { + branch := c.Params("*") + if !c.Repo.GitRepo.IsBranchExist(branch) { + c.NotFound() + return + } + + c.Data["Title"] = c.Tr("repo.settings.protected_branches") + " - " + branch + c.Data["PageIsSettingsBranches"] = true + + protectBranch, err := models.GetProtectBranchOfRepoByName(c.Repo.Repository.ID, branch) + if err != nil { + if !models.IsErrBranchNotExist(err) { + c.Handle(500, "GetProtectBranchOfRepoByName", err) + return + } + + // No options found, create defaults. + protectBranch = &models.ProtectBranch{ + Name: branch, + } + } + + if c.Repo.Owner.IsOrganization() { + users, err := c.Repo.Repository.GetWriters() + if err != nil { + c.Handle(500, "Repo.Repository.GetPushers", err) + return + } + c.Data["Users"] = users + c.Data["whitelist_users"] = protectBranch.WhitelistUserIDs + + teams, err := c.Repo.Owner.TeamsHaveAccessToRepo(c.Repo.Repository.ID, models.ACCESS_MODE_WRITE) + if err != nil { + c.Handle(500, "Repo.Owner.TeamsHaveAccessToRepo", err) + return + } + c.Data["Teams"] = teams + c.Data["whitelist_teams"] = protectBranch.WhitelistTeamIDs + } + + c.Data["Branch"] = protectBranch + c.HTML(200, SETTINGS_PROTECTED_BRANCH) +} + +func SettingsProtectedBranchPost(c *context.Context, f form.ProtectBranch) { + branch := c.Params("*") + if !c.Repo.GitRepo.IsBranchExist(branch) { + c.NotFound() + return + } + + protectBranch, err := models.GetProtectBranchOfRepoByName(c.Repo.Repository.ID, branch) + if err != nil { + if !models.IsErrBranchNotExist(err) { + c.Handle(500, "GetProtectBranchOfRepoByName", err) + return + } + + // No options found, create defaults. + protectBranch = &models.ProtectBranch{ + RepoID: c.Repo.Repository.ID, + Name: branch, + } + } + + protectBranch.Protected = f.Protected + protectBranch.RequirePullRequest = f.RequirePullRequest + protectBranch.EnableWhitelist = f.EnableWhitelist + if c.Repo.Owner.IsOrganization() { + err = models.UpdateOrgProtectBranch(c.Repo.Repository, protectBranch, f.WhitelistUsers, f.WhitelistTeams) + } else { + err = models.UpdateProtectBranch(protectBranch) + } + if err != nil { + c.Handle(500, "UpdateOrgProtectBranch/UpdateProtectBranch", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.update_protect_branch_success")) + c.Redirect(fmt.Sprintf("%s/settings/branches/%s", c.Repo.RepoLink, branch)) +} + +func SettingsGitHooks(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings.githooks") + c.Data["PageIsSettingsGitHooks"] = true + + hooks, err := c.Repo.GitRepo.Hooks() + if err != nil { + c.Handle(500, "Hooks", err) + return + } + c.Data["Hooks"] = hooks + + c.HTML(200, SETTINGS_GITHOOKS) +} + +func SettingsGitHooksEdit(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings.githooks") + c.Data["PageIsSettingsGitHooks"] = true + c.Data["RequireSimpleMDE"] = true + + name := c.Params(":name") + hook, err := c.Repo.GitRepo.GetHook(name) + if err != nil { + if err == git.ErrNotValidHook { + c.Handle(404, "GetHook", err) + } else { + c.Handle(500, "GetHook", err) + } + return + } + c.Data["Hook"] = hook + c.HTML(200, SETTINGS_GITHOOK_EDIT) +} + +func SettingsGitHooksEditPost(c *context.Context) { + name := c.Params(":name") + hook, err := c.Repo.GitRepo.GetHook(name) + if err != nil { + if err == git.ErrNotValidHook { + c.Handle(404, "GetHook", err) + } else { + c.Handle(500, "GetHook", err) + } + return + } + hook.Content = c.Query("content") + if err = hook.Update(); err != nil { + c.Handle(500, "hook.Update", err) + return + } + c.Redirect(c.Data["Link"].(string)) +} + +func SettingsDeployKeys(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings.deploy_keys") + c.Data["PageIsSettingsKeys"] = true + + keys, err := models.ListDeployKeys(c.Repo.Repository.ID) + if err != nil { + c.Handle(500, "ListDeployKeys", err) + return + } + c.Data["Deploykeys"] = keys + + c.HTML(200, SETTINGS_DEPLOY_KEYS) +} + +func SettingsDeployKeysPost(c *context.Context, f form.AddSSHKey) { + c.Data["Title"] = c.Tr("repo.settings.deploy_keys") + c.Data["PageIsSettingsKeys"] = true + + keys, err := models.ListDeployKeys(c.Repo.Repository.ID) + if err != nil { + c.Handle(500, "ListDeployKeys", err) + return + } + c.Data["Deploykeys"] = keys + + if c.HasError() { + c.HTML(200, SETTINGS_DEPLOY_KEYS) + return + } + + content, err := models.CheckPublicKeyString(f.Content) + if err != nil { + if models.IsErrKeyUnableVerify(err) { + c.Flash.Info(c.Tr("form.unable_verify_ssh_key")) + } else { + c.Data["HasError"] = true + c.Data["Err_Content"] = true + c.Flash.Error(c.Tr("form.invalid_ssh_key", err.Error())) + c.Redirect(c.Repo.RepoLink + "/settings/keys") + return + } + } + + key, err := models.AddDeployKey(c.Repo.Repository.ID, f.Title, content) + if err != nil { + c.Data["HasError"] = true + switch { + case models.IsErrKeyAlreadyExist(err): + c.Data["Err_Content"] = true + c.RenderWithErr(c.Tr("repo.settings.key_been_used"), SETTINGS_DEPLOY_KEYS, &f) + case models.IsErrKeyNameAlreadyUsed(err): + c.Data["Err_Title"] = true + c.RenderWithErr(c.Tr("repo.settings.key_name_used"), SETTINGS_DEPLOY_KEYS, &f) + default: + c.Handle(500, "AddDeployKey", err) + } + return + } + + log.Trace("Deploy key added: %d", c.Repo.Repository.ID) + c.Flash.Success(c.Tr("repo.settings.add_key_success", key.Name)) + c.Redirect(c.Repo.RepoLink + "/settings/keys") +} + +func DeleteDeployKey(c *context.Context) { + if err := models.DeleteDeployKey(c.User, c.QueryInt64("id")); err != nil { + c.Flash.Error("DeleteDeployKey: " + err.Error()) + } else { + c.Flash.Success(c.Tr("repo.settings.deploy_key_deletion_success")) + } + + c.JSON(200, map[string]interface{}{ + "redirect": c.Repo.RepoLink + "/settings/keys", + }) +} diff --git a/routes/repo/view.go b/routes/repo/view.go new file mode 100644 index 00000000..1ea25d51 --- /dev/null +++ b/routes/repo/view.go @@ -0,0 +1,367 @@ +// 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 ( + "bytes" + "fmt" + gotemplate "html/template" + "io/ioutil" + "path" + "strings" + + "github.com/Unknwon/paginater" + log "gopkg.in/clog.v1" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/markup" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/template" + "github.com/gogits/gogs/pkg/template/highlight" + "github.com/gogits/gogs/pkg/tool" +) + +const ( + BARE = "repo/bare" + HOME = "repo/home" + WATCHERS = "repo/watchers" + FORKS = "repo/forks" +) + +func renderDirectory(c *context.Context, treeLink string) { + tree, err := c.Repo.Commit.SubTree(c.Repo.TreePath) + if err != nil { + c.NotFoundOrServerError("Repo.Commit.SubTree", git.IsErrNotExist, err) + return + } + + entries, err := tree.ListEntries() + if err != nil { + c.ServerError("ListEntries", err) + return + } + entries.Sort() + + c.Data["Files"], err = entries.GetCommitsInfoWithCustomConcurrency(c.Repo.Commit, c.Repo.TreePath, setting.Repository.CommitsFetchConcurrency) + if err != nil { + c.ServerError("GetCommitsInfoWithCustomConcurrency", err) + return + } + + var readmeFile *git.Blob + for _, entry := range entries { + if entry.IsDir() || !markup.IsReadmeFile(entry.Name()) { + continue + } + + // TODO: collect all possible README files and show with priority. + readmeFile = entry.Blob() + break + } + + if readmeFile != nil { + c.Data["RawFileLink"] = "" + c.Data["ReadmeInList"] = true + c.Data["ReadmeExist"] = true + + dataRc, err := readmeFile.Data() + if err != nil { + c.ServerError("readmeFile.Data", err) + return + } + + buf := make([]byte, 1024) + n, _ := dataRc.Read(buf) + buf = buf[:n] + + isTextFile := tool.IsTextFile(buf) + c.Data["IsTextFile"] = isTextFile + c.Data["FileName"] = readmeFile.Name() + if isTextFile { + d, _ := ioutil.ReadAll(dataRc) + buf = append(buf, d...) + + switch markup.Detect(readmeFile.Name()) { + case markup.MARKDOWN: + c.Data["IsMarkdown"] = true + buf = markup.Markdown(buf, treeLink, c.Repo.Repository.ComposeMetas()) + case markup.ORG_MODE: + c.Data["IsMarkdown"] = true + buf = markup.OrgMode(buf, treeLink, c.Repo.Repository.ComposeMetas()) + case markup.IPYTHON_NOTEBOOK: + c.Data["IsIPythonNotebook"] = true + c.Data["RawFileLink"] = c.Repo.RepoLink + "/raw/" + path.Join(c.Repo.BranchName, c.Repo.TreePath, readmeFile.Name()) + default: + buf = bytes.Replace(buf, []byte("\n"), []byte(`<br>`), -1) + } + c.Data["FileContent"] = string(buf) + } + } + + // Show latest commit info of repository in table header, + // or of directory if not in root directory. + latestCommit := c.Repo.Commit + if len(c.Repo.TreePath) > 0 { + latestCommit, err = c.Repo.Commit.GetCommitByPath(c.Repo.TreePath) + if err != nil { + c.ServerError("GetCommitByPath", err) + return + } + } + c.Data["LatestCommit"] = latestCommit + c.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit) + + if c.Repo.CanEnableEditor() { + c.Data["CanAddFile"] = true + c.Data["CanUploadFile"] = setting.Repository.Upload.Enabled + } +} + +func renderFile(c *context.Context, entry *git.TreeEntry, treeLink, rawLink string) { + c.Data["IsViewFile"] = true + + blob := entry.Blob() + dataRc, err := blob.Data() + if err != nil { + c.Handle(500, "Data", err) + return + } + + c.Data["FileSize"] = blob.Size() + c.Data["FileName"] = blob.Name() + c.Data["HighlightClass"] = highlight.FileNameToHighlightClass(blob.Name()) + c.Data["RawFileLink"] = rawLink + "/" + c.Repo.TreePath + + buf := make([]byte, 1024) + n, _ := dataRc.Read(buf) + buf = buf[:n] + + isTextFile := tool.IsTextFile(buf) + c.Data["IsTextFile"] = isTextFile + + // Assume file is not editable first. + if !isTextFile { + c.Data["EditFileTooltip"] = c.Tr("repo.editor.cannot_edit_non_text_files") + } + + canEnableEditor := c.Repo.CanEnableEditor() + switch { + case isTextFile: + if blob.Size() >= setting.UI.MaxDisplayFileSize { + c.Data["IsFileTooLarge"] = true + break + } + + c.Data["ReadmeExist"] = markup.IsReadmeFile(blob.Name()) + + d, _ := ioutil.ReadAll(dataRc) + buf = append(buf, d...) + + switch markup.Detect(blob.Name()) { + case markup.MARKDOWN: + c.Data["IsMarkdown"] = true + c.Data["FileContent"] = string(markup.Markdown(buf, path.Dir(treeLink), c.Repo.Repository.ComposeMetas())) + case markup.ORG_MODE: + c.Data["IsMarkdown"] = true + c.Data["FileContent"] = string(markup.OrgMode(buf, path.Dir(treeLink), c.Repo.Repository.ComposeMetas())) + case markup.IPYTHON_NOTEBOOK: + c.Data["IsIPythonNotebook"] = true + default: + // Building code view blocks with line number on server side. + var fileContent string + if err, content := template.ToUTF8WithErr(buf); err != nil { + if err != nil { + log.Error(4, "ToUTF8WithErr: %s", err) + } + fileContent = string(buf) + } else { + fileContent = content + } + + var output bytes.Buffer + lines := strings.Split(fileContent, "\n") + for index, line := range lines { + output.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, index+1, index+1, gotemplate.HTMLEscapeString(strings.TrimRight(line, "\r"))) + "\n") + } + c.Data["FileContent"] = gotemplate.HTML(output.String()) + + output.Reset() + for i := 0; i < len(lines); i++ { + output.WriteString(fmt.Sprintf(`<span id="L%d">%d</span>`, i+1, i+1)) + } + c.Data["LineNums"] = gotemplate.HTML(output.String()) + } + + if canEnableEditor { + c.Data["CanEditFile"] = true + c.Data["EditFileTooltip"] = c.Tr("repo.editor.edit_this_file") + } else if !c.Repo.IsViewBranch { + c.Data["EditFileTooltip"] = c.Tr("repo.editor.must_be_on_a_branch") + } else if !c.Repo.IsWriter() { + c.Data["EditFileTooltip"] = c.Tr("repo.editor.fork_before_edit") + } + + case tool.IsPDFFile(buf): + c.Data["IsPDFFile"] = true + case tool.IsVideoFile(buf): + c.Data["IsVideoFile"] = true + case tool.IsImageFile(buf): + c.Data["IsImageFile"] = true + } + + if canEnableEditor { + c.Data["CanDeleteFile"] = true + c.Data["DeleteFileTooltip"] = c.Tr("repo.editor.delete_this_file") + } else if !c.Repo.IsViewBranch { + c.Data["DeleteFileTooltip"] = c.Tr("repo.editor.must_be_on_a_branch") + } else if !c.Repo.IsWriter() { + c.Data["DeleteFileTooltip"] = c.Tr("repo.editor.must_have_write_access") + } +} + +func setEditorconfigIfExists(c *context.Context) { + ec, err := c.Repo.GetEditorconfig() + if err != nil && !git.IsErrNotExist(err) { + log.Trace("setEditorconfigIfExists.GetEditorconfig [%d]: %v", c.Repo.Repository.ID, err) + return + } + c.Data["Editorconfig"] = ec +} + +func Home(c *context.Context) { + c.Data["PageIsViewFiles"] = true + + if c.Repo.Repository.IsBare { + c.HTML(200, BARE) + return + } + + title := c.Repo.Repository.Owner.Name + "/" + c.Repo.Repository.Name + if len(c.Repo.Repository.Description) > 0 { + title += ": " + c.Repo.Repository.Description + } + c.Data["Title"] = title + if c.Repo.BranchName != c.Repo.Repository.DefaultBranch { + c.Data["Title"] = title + " @ " + c.Repo.BranchName + } + c.Data["RequireHighlightJS"] = true + + branchLink := c.Repo.RepoLink + "/src/" + c.Repo.BranchName + treeLink := branchLink + rawLink := c.Repo.RepoLink + "/raw/" + c.Repo.BranchName + + isRootDir := false + if len(c.Repo.TreePath) > 0 { + treeLink += "/" + c.Repo.TreePath + } else { + isRootDir = true + + // Only show Git stats panel when view root directory + var err error + c.Repo.CommitsCount, err = c.Repo.Commit.CommitsCount() + if err != nil { + c.Handle(500, "CommitsCount", err) + return + } + c.Data["CommitsCount"] = c.Repo.CommitsCount + } + c.Data["PageIsRepoHome"] = isRootDir + + // Get current entry user currently looking at. + entry, err := c.Repo.Commit.GetTreeEntryByPath(c.Repo.TreePath) + if err != nil { + c.NotFoundOrServerError("Repo.Commit.GetTreeEntryByPath", git.IsErrNotExist, err) + return + } + + if entry.IsDir() { + renderDirectory(c, treeLink) + } else { + renderFile(c, entry, treeLink, rawLink) + } + if c.Written() { + return + } + + setEditorconfigIfExists(c) + if c.Written() { + return + } + + var treeNames []string + paths := make([]string, 0, 5) + if len(c.Repo.TreePath) > 0 { + treeNames = strings.Split(c.Repo.TreePath, "/") + for i := range treeNames { + paths = append(paths, strings.Join(treeNames[:i+1], "/")) + } + + c.Data["HasParentPath"] = true + if len(paths)-2 >= 0 { + c.Data["ParentPath"] = "/" + paths[len(paths)-2] + } + } + + c.Data["Paths"] = paths + c.Data["TreeLink"] = treeLink + c.Data["TreeNames"] = treeNames + c.Data["BranchLink"] = branchLink + c.HTML(200, HOME) +} + +func RenderUserCards(c *context.Context, total int, getter func(page int) ([]*models.User, error), tpl string) { + page := c.QueryInt("page") + if page <= 0 { + page = 1 + } + pager := paginater.New(total, models.ItemsPerPage, page, 5) + c.Data["Page"] = pager + + items, err := getter(pager.Current()) + if err != nil { + c.Handle(500, "getter", err) + return + } + c.Data["Cards"] = items + + c.HTML(200, tpl) +} + +func Watchers(c *context.Context) { + c.Data["Title"] = c.Tr("repo.watchers") + c.Data["CardsTitle"] = c.Tr("repo.watchers") + c.Data["PageIsWatchers"] = true + RenderUserCards(c, c.Repo.Repository.NumWatches, c.Repo.Repository.GetWatchers, WATCHERS) +} + +func Stars(c *context.Context) { + c.Data["Title"] = c.Tr("repo.stargazers") + c.Data["CardsTitle"] = c.Tr("repo.stargazers") + c.Data["PageIsStargazers"] = true + RenderUserCards(c, c.Repo.Repository.NumStars, c.Repo.Repository.GetStargazers, WATCHERS) +} + +func Forks(c *context.Context) { + c.Data["Title"] = c.Tr("repos.forks") + + forks, err := c.Repo.Repository.GetForks() + if err != nil { + c.Handle(500, "GetForks", err) + return + } + + for _, fork := range forks { + if err = fork.GetOwner(); err != nil { + c.Handle(500, "GetOwner", err) + return + } + } + c.Data["Forks"] = forks + + c.HTML(200, FORKS) +} diff --git a/routes/repo/webhook.go b/routes/repo/webhook.go new file mode 100644 index 00000000..c572d446 --- /dev/null +++ b/routes/repo/webhook.go @@ -0,0 +1,558 @@ +// 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 ( + "encoding/json" + "fmt" + "strings" + + "github.com/Unknwon/com" + + git "github.com/gogits/git-module" + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/setting" +) + +const ( + WEBHOOKS = "repo/settings/webhook/base" + WEBHOOK_NEW = "repo/settings/webhook/new" + ORG_WEBHOOK_NEW = "org/settings/webhook_new" +) + +func Webhooks(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings.hooks") + c.Data["PageIsSettingsHooks"] = true + c.Data["BaseLink"] = c.Repo.RepoLink + c.Data["Description"] = c.Tr("repo.settings.hooks_desc", "https://github.com/gogits/go-gogs-client/wiki/Repositories-Webhooks") + c.Data["Types"] = setting.Webhook.Types + + ws, err := models.GetWebhooksByRepoID(c.Repo.Repository.ID) + if err != nil { + c.Handle(500, "GetWebhooksByRepoID", err) + return + } + c.Data["Webhooks"] = ws + + c.HTML(200, WEBHOOKS) +} + +type OrgRepoCtx struct { + OrgID int64 + RepoID int64 + Link string + NewTemplate string +} + +// getOrgRepoCtx determines whether this is a repo context or organization context. +func getOrgRepoCtx(c *context.Context) (*OrgRepoCtx, error) { + if len(c.Repo.RepoLink) > 0 { + c.Data["PageIsRepositoryContext"] = true + return &OrgRepoCtx{ + RepoID: c.Repo.Repository.ID, + Link: c.Repo.RepoLink, + NewTemplate: WEBHOOK_NEW, + }, nil + } + + if len(c.Org.OrgLink) > 0 { + c.Data["PageIsOrganizationContext"] = true + return &OrgRepoCtx{ + OrgID: c.Org.Organization.ID, + Link: c.Org.OrgLink, + NewTemplate: ORG_WEBHOOK_NEW, + }, nil + } + + return nil, errors.New("Unable to set OrgRepo context") +} + +func checkHookType(c *context.Context) string { + hookType := strings.ToLower(c.Params(":type")) + if !com.IsSliceContainsStr(setting.Webhook.Types, hookType) { + c.Handle(404, "checkHookType", nil) + return "" + } + return hookType +} + +func WebhooksNew(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings.add_webhook") + c.Data["PageIsSettingsHooks"] = true + c.Data["PageIsSettingsHooksNew"] = true + c.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}} + + orCtx, err := getOrgRepoCtx(c) + if err != nil { + c.Handle(500, "getOrgRepoCtx", err) + return + } + + c.Data["HookType"] = checkHookType(c) + if c.Written() { + return + } + c.Data["BaseLink"] = orCtx.Link + + c.HTML(200, orCtx.NewTemplate) +} + +func ParseHookEvent(f form.Webhook) *models.HookEvent { + return &models.HookEvent{ + PushOnly: f.PushOnly(), + SendEverything: f.SendEverything(), + ChooseEvents: f.ChooseEvents(), + HookEvents: models.HookEvents{ + Create: f.Create, + Delete: f.Delete, + Fork: f.Fork, + Push: f.Push, + Issues: f.Issues, + IssueComment: f.IssueComment, + PullRequest: f.PullRequest, + Release: f.Release, + }, + } +} + +func WebHooksNewPost(c *context.Context, f form.NewWebhook) { + c.Data["Title"] = c.Tr("repo.settings.add_webhook") + c.Data["PageIsSettingsHooks"] = true + c.Data["PageIsSettingsHooksNew"] = true + c.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}} + c.Data["HookType"] = "gogs" + + orCtx, err := getOrgRepoCtx(c) + if err != nil { + c.Handle(500, "getOrgRepoCtx", err) + return + } + c.Data["BaseLink"] = orCtx.Link + + if c.HasError() { + c.HTML(200, orCtx.NewTemplate) + return + } + + contentType := models.JSON + if models.HookContentType(f.ContentType) == models.FORM { + contentType = models.FORM + } + + w := &models.Webhook{ + RepoID: orCtx.RepoID, + URL: f.PayloadURL, + ContentType: contentType, + Secret: f.Secret, + HookEvent: ParseHookEvent(f.Webhook), + IsActive: f.Active, + HookTaskType: models.GOGS, + OrgID: orCtx.OrgID, + } + if err := w.UpdateEvent(); err != nil { + c.Handle(500, "UpdateEvent", err) + return + } else if err := models.CreateWebhook(w); err != nil { + c.Handle(500, "CreateWebhook", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.add_hook_success")) + c.Redirect(orCtx.Link + "/settings/hooks") +} + +func SlackHooksNewPost(c *context.Context, f form.NewSlackHook) { + c.Data["Title"] = c.Tr("repo.settings") + c.Data["PageIsSettingsHooks"] = true + c.Data["PageIsSettingsHooksNew"] = true + c.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}} + + orCtx, err := getOrgRepoCtx(c) + if err != nil { + c.Handle(500, "getOrgRepoCtx", err) + return + } + + if c.HasError() { + c.HTML(200, orCtx.NewTemplate) + return + } + + meta, err := json.Marshal(&models.SlackMeta{ + Channel: f.Channel, + Username: f.Username, + IconURL: f.IconURL, + Color: f.Color, + }) + if err != nil { + c.Handle(500, "Marshal", err) + return + } + + w := &models.Webhook{ + RepoID: orCtx.RepoID, + URL: f.PayloadURL, + ContentType: models.JSON, + HookEvent: ParseHookEvent(f.Webhook), + IsActive: f.Active, + HookTaskType: models.SLACK, + Meta: string(meta), + OrgID: orCtx.OrgID, + } + if err := w.UpdateEvent(); err != nil { + c.Handle(500, "UpdateEvent", err) + return + } else if err := models.CreateWebhook(w); err != nil { + c.Handle(500, "CreateWebhook", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.add_hook_success")) + c.Redirect(orCtx.Link + "/settings/hooks") +} + +// FIXME: merge logic to Slack +func DiscordHooksNewPost(c *context.Context, f form.NewDiscordHook) { + c.Data["Title"] = c.Tr("repo.settings") + c.Data["PageIsSettingsHooks"] = true + c.Data["PageIsSettingsHooksNew"] = true + c.Data["Webhook"] = models.Webhook{HookEvent: &models.HookEvent{}} + + orCtx, err := getOrgRepoCtx(c) + if err != nil { + c.Handle(500, "getOrgRepoCtx", err) + return + } + + if c.HasError() { + c.HTML(200, orCtx.NewTemplate) + return + } + + meta, err := json.Marshal(&models.SlackMeta{ + Username: f.Username, + IconURL: f.IconURL, + Color: f.Color, + }) + if err != nil { + c.Handle(500, "Marshal", err) + return + } + + w := &models.Webhook{ + RepoID: orCtx.RepoID, + URL: f.PayloadURL, + ContentType: models.JSON, + HookEvent: ParseHookEvent(f.Webhook), + IsActive: f.Active, + HookTaskType: models.DISCORD, + Meta: string(meta), + OrgID: orCtx.OrgID, + } + if err := w.UpdateEvent(); err != nil { + c.Handle(500, "UpdateEvent", err) + return + } else if err := models.CreateWebhook(w); err != nil { + c.Handle(500, "CreateWebhook", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.add_hook_success")) + c.Redirect(orCtx.Link + "/settings/hooks") +} + +func checkWebhook(c *context.Context) (*OrgRepoCtx, *models.Webhook) { + c.Data["RequireHighlightJS"] = true + + orCtx, err := getOrgRepoCtx(c) + if err != nil { + c.Handle(500, "getOrgRepoCtx", err) + return nil, nil + } + c.Data["BaseLink"] = orCtx.Link + + var w *models.Webhook + if orCtx.RepoID > 0 { + w, err = models.GetWebhookOfRepoByID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + } else { + w, err = models.GetWebhookByOrgID(c.Org.Organization.ID, c.ParamsInt64(":id")) + } + if err != nil { + c.NotFoundOrServerError("GetWebhookOfRepoByID/GetWebhookByOrgID", errors.IsWebhookNotExist, err) + return nil, nil + } + + switch w.HookTaskType { + case models.SLACK: + c.Data["SlackHook"] = w.GetSlackHook() + c.Data["HookType"] = "slack" + case models.DISCORD: + c.Data["SlackHook"] = w.GetSlackHook() + c.Data["HookType"] = "discord" + default: + c.Data["HookType"] = "gogs" + } + + c.Data["History"], err = w.History(1) + if err != nil { + c.Handle(500, "History", err) + } + return orCtx, w +} + +func WebHooksEdit(c *context.Context) { + c.Data["Title"] = c.Tr("repo.settings.update_webhook") + c.Data["PageIsSettingsHooks"] = true + c.Data["PageIsSettingsHooksEdit"] = true + + orCtx, w := checkWebhook(c) + if c.Written() { + return + } + c.Data["Webhook"] = w + + c.HTML(200, orCtx.NewTemplate) +} + +func WebHooksEditPost(c *context.Context, f form.NewWebhook) { + c.Data["Title"] = c.Tr("repo.settings.update_webhook") + c.Data["PageIsSettingsHooks"] = true + c.Data["PageIsSettingsHooksEdit"] = true + + orCtx, w := checkWebhook(c) + if c.Written() { + return + } + c.Data["Webhook"] = w + + if c.HasError() { + c.HTML(200, orCtx.NewTemplate) + return + } + + contentType := models.JSON + if models.HookContentType(f.ContentType) == models.FORM { + contentType = models.FORM + } + + w.URL = f.PayloadURL + w.ContentType = contentType + w.Secret = f.Secret + w.HookEvent = ParseHookEvent(f.Webhook) + w.IsActive = f.Active + if err := w.UpdateEvent(); err != nil { + c.Handle(500, "UpdateEvent", err) + return + } else if err := models.UpdateWebhook(w); err != nil { + c.Handle(500, "WebHooksEditPost", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.update_hook_success")) + c.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID)) +} + +func SlackHooksEditPost(c *context.Context, f form.NewSlackHook) { + c.Data["Title"] = c.Tr("repo.settings") + c.Data["PageIsSettingsHooks"] = true + c.Data["PageIsSettingsHooksEdit"] = true + + orCtx, w := checkWebhook(c) + if c.Written() { + return + } + c.Data["Webhook"] = w + + if c.HasError() { + c.HTML(200, orCtx.NewTemplate) + return + } + + meta, err := json.Marshal(&models.SlackMeta{ + Channel: f.Channel, + Username: f.Username, + IconURL: f.IconURL, + Color: f.Color, + }) + if err != nil { + c.Handle(500, "Marshal", err) + return + } + + w.URL = f.PayloadURL + w.Meta = string(meta) + w.HookEvent = ParseHookEvent(f.Webhook) + w.IsActive = f.Active + if err := w.UpdateEvent(); err != nil { + c.Handle(500, "UpdateEvent", err) + return + } else if err := models.UpdateWebhook(w); err != nil { + c.Handle(500, "UpdateWebhook", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.update_hook_success")) + c.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID)) +} + +// FIXME: merge logic to Slack +func DiscordHooksEditPost(c *context.Context, f form.NewDiscordHook) { + c.Data["Title"] = c.Tr("repo.settings") + c.Data["PageIsSettingsHooks"] = true + c.Data["PageIsSettingsHooksEdit"] = true + + orCtx, w := checkWebhook(c) + if c.Written() { + return + } + c.Data["Webhook"] = w + + if c.HasError() { + c.HTML(200, orCtx.NewTemplate) + return + } + + meta, err := json.Marshal(&models.SlackMeta{ + Username: f.Username, + IconURL: f.IconURL, + Color: f.Color, + }) + if err != nil { + c.Handle(500, "Marshal", err) + return + } + + w.URL = f.PayloadURL + w.Meta = string(meta) + w.HookEvent = ParseHookEvent(f.Webhook) + w.IsActive = f.Active + if err := w.UpdateEvent(); err != nil { + c.Handle(500, "UpdateEvent", err) + return + } else if err := models.UpdateWebhook(w); err != nil { + c.Handle(500, "UpdateWebhook", err) + return + } + + c.Flash.Success(c.Tr("repo.settings.update_hook_success")) + c.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID)) +} + +func TestWebhook(c *context.Context) { + var authorUsername, committerUsername string + + // Grab latest commit or fake one if it's empty repository. + commit := c.Repo.Commit + if commit == nil { + ghost := models.NewGhostUser() + commit = &git.Commit{ + ID: git.MustIDFromString(git.EMPTY_SHA), + Author: ghost.NewGitSig(), + Committer: ghost.NewGitSig(), + CommitMessage: "This is a fake commit", + } + authorUsername = ghost.Name + committerUsername = ghost.Name + } else { + // Try to match email with a real user. + author, err := models.GetUserByEmail(commit.Author.Email) + if err == nil { + authorUsername = author.Name + } else if !errors.IsUserNotExist(err) { + c.Handle(500, "GetUserByEmail.(author)", err) + return + } + + committer, err := models.GetUserByEmail(commit.Committer.Email) + if err == nil { + committerUsername = committer.Name + } else if !errors.IsUserNotExist(err) { + c.Handle(500, "GetUserByEmail.(committer)", err) + return + } + } + + fileStatus, err := commit.FileStatus() + if err != nil { + c.Handle(500, "FileStatus", err) + return + } + + apiUser := c.User.APIFormat() + p := &api.PushPayload{ + Ref: git.BRANCH_PREFIX + c.Repo.Repository.DefaultBranch, + Before: commit.ID.String(), + After: commit.ID.String(), + Commits: []*api.PayloadCommit{ + { + ID: commit.ID.String(), + Message: commit.Message(), + URL: c.Repo.Repository.HTMLURL() + "/commit/" + commit.ID.String(), + Author: &api.PayloadUser{ + Name: commit.Author.Name, + Email: commit.Author.Email, + UserName: authorUsername, + }, + Committer: &api.PayloadUser{ + Name: commit.Committer.Name, + Email: commit.Committer.Email, + UserName: committerUsername, + }, + Added: fileStatus.Added, + Removed: fileStatus.Removed, + Modified: fileStatus.Modified, + }, + }, + Repo: c.Repo.Repository.APIFormat(nil), + Pusher: apiUser, + Sender: apiUser, + } + if err := models.TestWebhook(c.Repo.Repository, models.HOOK_EVENT_PUSH, p, c.ParamsInt64("id")); err != nil { + c.Handle(500, "TestWebhook", err) + } else { + c.Flash.Info(c.Tr("repo.settings.webhook.test_delivery_success")) + c.Status(200) + } +} + +func RedeliveryWebhook(c *context.Context) { + webhook, err := models.GetWebhookOfRepoByID(c.Repo.Repository.ID, c.ParamsInt64(":id")) + if err != nil { + c.NotFoundOrServerError("GetWebhookOfRepoByID/GetWebhookByOrgID", errors.IsWebhookNotExist, err) + return + } + + hookTask, err := models.GetHookTaskOfWebhookByUUID(webhook.ID, c.Query("uuid")) + if err != nil { + c.NotFoundOrServerError("GetHookTaskOfWebhookByUUID/GetWebhookByOrgID", errors.IsHookTaskNotExist, err) + return + } + + hookTask.IsDelivered = false + if err = models.UpdateHookTask(hookTask); err != nil { + c.Handle(500, "UpdateHookTask", err) + } else { + go models.HookQueue.Add(c.Repo.Repository.ID) + c.Flash.Info(c.Tr("repo.settings.webhook.redelivery_success", hookTask.UUID)) + c.Status(200) + } +} + +func DeleteWebhook(c *context.Context) { + if err := models.DeleteWebhookOfRepoByID(c.Repo.Repository.ID, c.QueryInt64("id")); err != nil { + c.Flash.Error("DeleteWebhookByRepoID: " + err.Error()) + } else { + c.Flash.Success(c.Tr("repo.settings.webhook_deletion_success")) + } + + c.JSON(200, map[string]interface{}{ + "redirect": c.Repo.RepoLink + "/settings/hooks", + }) +} diff --git a/routes/repo/wiki.go b/routes/repo/wiki.go new file mode 100644 index 00000000..ad2cfbae --- /dev/null +++ b/routes/repo/wiki.go @@ -0,0 +1,274 @@ +// 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 ( + "io/ioutil" + "strings" + "time" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/markup" +) + +const ( + WIKI_START = "repo/wiki/start" + WIKI_VIEW = "repo/wiki/view" + WIKI_NEW = "repo/wiki/new" + WIKI_PAGES = "repo/wiki/pages" +) + +func MustEnableWiki(c *context.Context) { + if !c.Repo.Repository.EnableWiki { + c.Handle(404, "MustEnableWiki", nil) + return + } + + if c.Repo.Repository.EnableExternalWiki { + c.Redirect(c.Repo.Repository.ExternalWikiURL) + return + } +} + +type PageMeta struct { + Name string + URL string + Updated time.Time +} + +func renderWikiPage(c *context.Context, isViewPage bool) (*git.Repository, string) { + wikiRepo, err := git.OpenRepository(c.Repo.Repository.WikiPath()) + if err != nil { + c.Handle(500, "OpenRepository", err) + return nil, "" + } + commit, err := wikiRepo.GetBranchCommit("master") + if err != nil { + c.Handle(500, "GetBranchCommit", err) + return nil, "" + } + + // Get page list. + if isViewPage { + entries, err := commit.ListEntries() + if err != nil { + c.Handle(500, "ListEntries", err) + return nil, "" + } + pages := make([]PageMeta, 0, len(entries)) + for i := range entries { + if entries[i].Type == git.OBJECT_BLOB && strings.HasSuffix(entries[i].Name(), ".md") { + name := strings.TrimSuffix(entries[i].Name(), ".md") + pages = append(pages, PageMeta{ + Name: name, + URL: models.ToWikiPageURL(name), + }) + } + } + c.Data["Pages"] = pages + } + + pageURL := c.Params(":page") + if len(pageURL) == 0 { + pageURL = "Home" + } + c.Data["PageURL"] = pageURL + + pageName := models.ToWikiPageName(pageURL) + c.Data["old_title"] = pageName + c.Data["Title"] = pageName + c.Data["title"] = pageName + c.Data["RequireHighlightJS"] = true + + blob, err := commit.GetBlobByPath(pageName + ".md") + if err != nil { + if git.IsErrNotExist(err) { + c.Redirect(c.Repo.RepoLink + "/wiki/_pages") + } else { + c.Handle(500, "GetBlobByPath", err) + } + return nil, "" + } + r, err := blob.Data() + if err != nil { + c.Handle(500, "Data", err) + return nil, "" + } + data, err := ioutil.ReadAll(r) + if err != nil { + c.Handle(500, "ReadAll", err) + return nil, "" + } + if isViewPage { + c.Data["content"] = string(markup.Markdown(data, c.Repo.RepoLink, c.Repo.Repository.ComposeMetas())) + } else { + c.Data["content"] = string(data) + } + + return wikiRepo, pageName +} + +func Wiki(c *context.Context) { + c.Data["PageIsWiki"] = true + + if !c.Repo.Repository.HasWiki() { + c.Data["Title"] = c.Tr("repo.wiki") + c.HTML(200, WIKI_START) + return + } + + wikiRepo, pageName := renderWikiPage(c, true) + if c.Written() { + return + } + + // Get last change information. + lastCommit, err := wikiRepo.GetCommitByPath(pageName + ".md") + if err != nil { + c.Handle(500, "GetCommitByPath", err) + return + } + c.Data["Author"] = lastCommit.Author + + c.HTML(200, WIKI_VIEW) +} + +func WikiPages(c *context.Context) { + c.Data["Title"] = c.Tr("repo.wiki.pages") + c.Data["PageIsWiki"] = true + + if !c.Repo.Repository.HasWiki() { + c.Redirect(c.Repo.RepoLink + "/wiki") + return + } + + wikiRepo, err := git.OpenRepository(c.Repo.Repository.WikiPath()) + if err != nil { + c.Handle(500, "OpenRepository", err) + return + } + commit, err := wikiRepo.GetBranchCommit("master") + if err != nil { + c.Handle(500, "GetBranchCommit", err) + return + } + + entries, err := commit.ListEntries() + if err != nil { + c.Handle(500, "ListEntries", err) + return + } + pages := make([]PageMeta, 0, len(entries)) + for i := range entries { + if entries[i].Type == git.OBJECT_BLOB && strings.HasSuffix(entries[i].Name(), ".md") { + commit, err := wikiRepo.GetCommitByPath(entries[i].Name()) + if err != nil { + c.ServerError("GetCommitByPath", err) + return + } + name := strings.TrimSuffix(entries[i].Name(), ".md") + pages = append(pages, PageMeta{ + Name: name, + URL: models.ToWikiPageURL(name), + Updated: commit.Author.When, + }) + } + } + c.Data["Pages"] = pages + + c.HTML(200, WIKI_PAGES) +} + +func NewWiki(c *context.Context) { + c.Data["Title"] = c.Tr("repo.wiki.new_page") + c.Data["PageIsWiki"] = true + c.Data["RequireSimpleMDE"] = true + + if !c.Repo.Repository.HasWiki() { + c.Data["title"] = "Home" + } + + c.HTML(200, WIKI_NEW) +} + +func NewWikiPost(c *context.Context, f form.NewWiki) { + c.Data["Title"] = c.Tr("repo.wiki.new_page") + c.Data["PageIsWiki"] = true + c.Data["RequireSimpleMDE"] = true + + if c.HasError() { + c.HTML(200, WIKI_NEW) + return + } + + if err := c.Repo.Repository.AddWikiPage(c.User, f.Title, f.Content, f.Message); err != nil { + if models.IsErrWikiAlreadyExist(err) { + c.Data["Err_Title"] = true + c.RenderWithErr(c.Tr("repo.wiki.page_already_exists"), WIKI_NEW, &f) + } else { + c.Handle(500, "AddWikiPage", err) + } + return + } + + c.Redirect(c.Repo.RepoLink + "/wiki/" + models.ToWikiPageURL(models.ToWikiPageName(f.Title))) +} + +func EditWiki(c *context.Context) { + c.Data["PageIsWiki"] = true + c.Data["PageIsWikiEdit"] = true + c.Data["RequireSimpleMDE"] = true + + if !c.Repo.Repository.HasWiki() { + c.Redirect(c.Repo.RepoLink + "/wiki") + return + } + + renderWikiPage(c, false) + if c.Written() { + return + } + + c.HTML(200, WIKI_NEW) +} + +func EditWikiPost(c *context.Context, f form.NewWiki) { + c.Data["Title"] = c.Tr("repo.wiki.new_page") + c.Data["PageIsWiki"] = true + c.Data["RequireSimpleMDE"] = true + + if c.HasError() { + c.HTML(200, WIKI_NEW) + return + } + + if err := c.Repo.Repository.EditWikiPage(c.User, f.OldTitle, f.Title, f.Content, f.Message); err != nil { + c.Handle(500, "EditWikiPage", err) + return + } + + c.Redirect(c.Repo.RepoLink + "/wiki/" + models.ToWikiPageURL(models.ToWikiPageName(f.Title))) +} + +func DeleteWikiPagePost(c *context.Context) { + pageURL := c.Params(":page") + if len(pageURL) == 0 { + pageURL = "Home" + } + + pageName := models.ToWikiPageName(pageURL) + if err := c.Repo.Repository.DeleteWikiPage(c.User, pageName); err != nil { + c.Handle(500, "DeleteWikiPage", err) + return + } + + c.JSON(200, map[string]interface{}{ + "redirect": c.Repo.RepoLink + "/wiki/", + }) +} diff --git a/routes/user/auth.go b/routes/user/auth.go new file mode 100644 index 00000000..34fdbd85 --- /dev/null +++ b/routes/user/auth.go @@ -0,0 +1,534 @@ +// 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 ( + "fmt" + "net/url" + + "github.com/go-macaron/captcha" + log "gopkg.in/clog.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/mailer" + "github.com/gogits/gogs/pkg/setting" +) + +const ( + LOGIN = "user/auth/login" + TWO_FACTOR = "user/auth/two_factor" + TWO_FACTOR_RECOVERY_CODE = "user/auth/two_factor_recovery_code" + SIGNUP = "user/auth/signup" + ACTIVATE = "user/auth/activate" + FORGOT_PASSWORD = "user/auth/forgot_passwd" + RESET_PASSWORD = "user/auth/reset_passwd" +) + +// AutoLogin reads cookie and try to auto-login. +func AutoLogin(c *context.Context) (bool, error) { + if !models.HasEngine { + return false, nil + } + + uname := c.GetCookie(setting.CookieUserName) + if len(uname) == 0 { + return false, nil + } + + isSucceed := false + defer func() { + if !isSucceed { + log.Trace("auto-login cookie cleared: %s", uname) + c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubURL) + c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubURL) + c.SetCookie(setting.LoginStatusCookieName, "", -1, setting.AppSubURL) + } + }() + + u, err := models.GetUserByName(uname) + if err != nil { + if !errors.IsUserNotExist(err) { + return false, fmt.Errorf("GetUserByName: %v", err) + } + return false, nil + } + + if val, ok := c.GetSuperSecureCookie(u.Rands+u.Passwd, setting.CookieRememberName); !ok || val != u.Name { + return false, nil + } + + isSucceed = true + c.Session.Set("uid", u.ID) + c.Session.Set("uname", u.Name) + c.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL) + if setting.EnableLoginStatusCookie { + c.SetCookie(setting.LoginStatusCookieName, "true", 0, setting.AppSubURL) + } + return true, nil +} + +// isValidRedirect returns false if the URL does not redirect to same site. +// False: //url, http://url +// True: /url +func isValidRedirect(url string) bool { + return len(url) >= 2 && url[0] == '/' && url[1] != '/' +} + +func Login(c *context.Context) { + c.Data["Title"] = c.Tr("sign_in") + + // Check auto-login. + isSucceed, err := AutoLogin(c) + if err != nil { + c.Handle(500, "AutoLogin", err) + return + } + + redirectTo := c.Query("redirect_to") + if len(redirectTo) > 0 { + c.SetCookie("redirect_to", redirectTo, 0, setting.AppSubURL) + } else { + redirectTo, _ = url.QueryUnescape(c.GetCookie("redirect_to")) + } + c.SetCookie("redirect_to", "", -1, setting.AppSubURL) + + if isSucceed { + if isValidRedirect(redirectTo) { + c.Redirect(redirectTo) + } else { + c.Redirect(setting.AppSubURL + "/") + } + return + } + + c.HTML(200, LOGIN) +} + +func afterLogin(c *context.Context, u *models.User, remember bool) { + if remember { + days := 86400 * setting.LoginRememberDays + c.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubURL, "", setting.CookieSecure, true) + c.SetSuperSecureCookie(u.Rands+u.Passwd, setting.CookieRememberName, u.Name, days, setting.AppSubURL, "", setting.CookieSecure, true) + } + + c.Session.Set("uid", u.ID) + c.Session.Set("uname", u.Name) + c.Session.Delete("twoFactorRemember") + c.Session.Delete("twoFactorUserID") + + // Clear whatever CSRF has right now, force to generate a new one + c.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL) + if setting.EnableLoginStatusCookie { + c.SetCookie(setting.LoginStatusCookieName, "true", 0, setting.AppSubURL) + } + + redirectTo, _ := url.QueryUnescape(c.GetCookie("redirect_to")) + c.SetCookie("redirect_to", "", -1, setting.AppSubURL) + if isValidRedirect(redirectTo) { + c.Redirect(redirectTo) + return + } + + c.Redirect(setting.AppSubURL + "/") +} + +func LoginPost(c *context.Context, f form.SignIn) { + c.Data["Title"] = c.Tr("sign_in") + + if c.HasError() { + c.Success(LOGIN) + return + } + + u, err := models.UserSignIn(f.UserName, f.Password) + if err != nil { + if errors.IsUserNotExist(err) { + c.RenderWithErr(c.Tr("form.username_password_incorrect"), LOGIN, &f) + } else { + c.ServerError("UserSignIn", err) + } + return + } + + if !u.IsEnabledTwoFactor() { + afterLogin(c, u, f.Remember) + return + } + + c.Session.Set("twoFactorRemember", f.Remember) + c.Session.Set("twoFactorUserID", u.ID) + c.Redirect(setting.AppSubURL + "/user/login/two_factor") +} + +func LoginTwoFactor(c *context.Context) { + _, ok := c.Session.Get("twoFactorUserID").(int64) + if !ok { + c.NotFound() + return + } + + c.Success(TWO_FACTOR) +} + +func LoginTwoFactorPost(c *context.Context) { + userID, ok := c.Session.Get("twoFactorUserID").(int64) + if !ok { + c.NotFound() + return + } + + t, err := models.GetTwoFactorByUserID(userID) + if err != nil { + c.ServerError("GetTwoFactorByUserID", err) + return + } + valid, err := t.ValidateTOTP(c.Query("passcode")) + if err != nil { + c.ServerError("ValidateTOTP", err) + return + } else if !valid { + c.Flash.Error(c.Tr("settings.two_factor_invalid_passcode")) + c.Redirect(setting.AppSubURL + "/user/login/two_factor") + return + } + + u, err := models.GetUserByID(userID) + if err != nil { + c.ServerError("GetUserByID", err) + return + } + afterLogin(c, u, c.Session.Get("twoFactorRemember").(bool)) +} + +func LoginTwoFactorRecoveryCode(c *context.Context) { + _, ok := c.Session.Get("twoFactorUserID").(int64) + if !ok { + c.NotFound() + return + } + + c.Success(TWO_FACTOR_RECOVERY_CODE) +} + +func LoginTwoFactorRecoveryCodePost(c *context.Context) { + userID, ok := c.Session.Get("twoFactorUserID").(int64) + if !ok { + c.NotFound() + return + } + + if err := models.UseRecoveryCode(userID, c.Query("recovery_code")); err != nil { + if errors.IsTwoFactorRecoveryCodeNotFound(err) { + c.Flash.Error(c.Tr("auth.login_two_factor_invalid_recovery_code")) + c.Redirect(setting.AppSubURL + "/user/login/two_factor_recovery_code") + } else { + c.ServerError("UseRecoveryCode", err) + } + return + } + + u, err := models.GetUserByID(userID) + if err != nil { + c.ServerError("GetUserByID", err) + return + } + afterLogin(c, u, c.Session.Get("twoFactorRemember").(bool)) +} + +func SignOut(c *context.Context) { + c.Session.Delete("uid") + c.Session.Delete("uname") + c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubURL) + c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubURL) + c.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL) + c.Redirect(setting.AppSubURL + "/") +} + +func SignUp(c *context.Context) { + c.Data["Title"] = c.Tr("sign_up") + + c.Data["EnableCaptcha"] = setting.Service.EnableCaptcha + + if setting.Service.DisableRegistration { + c.Data["DisableRegistration"] = true + c.HTML(200, SIGNUP) + return + } + + c.HTML(200, SIGNUP) +} + +func SignUpPost(c *context.Context, cpt *captcha.Captcha, f form.Register) { + c.Data["Title"] = c.Tr("sign_up") + + c.Data["EnableCaptcha"] = setting.Service.EnableCaptcha + + if setting.Service.DisableRegistration { + c.Error(403) + return + } + + if c.HasError() { + c.HTML(200, SIGNUP) + return + } + + if setting.Service.EnableCaptcha && !cpt.VerifyReq(c.Req) { + c.Data["Err_Captcha"] = true + c.RenderWithErr(c.Tr("form.captcha_incorrect"), SIGNUP, &f) + return + } + + if f.Password != f.Retype { + c.Data["Err_Password"] = true + c.RenderWithErr(c.Tr("form.password_not_match"), SIGNUP, &f) + return + } + + u := &models.User{ + Name: f.UserName, + Email: f.Email, + Passwd: f.Password, + IsActive: !setting.Service.RegisterEmailConfirm, + } + if err := models.CreateUser(u); err != nil { + switch { + case models.IsErrUserAlreadyExist(err): + c.Data["Err_UserName"] = true + c.RenderWithErr(c.Tr("form.username_been_taken"), SIGNUP, &f) + case models.IsErrEmailAlreadyUsed(err): + c.Data["Err_Email"] = true + c.RenderWithErr(c.Tr("form.email_been_used"), SIGNUP, &f) + case models.IsErrNameReserved(err): + c.Data["Err_UserName"] = true + c.RenderWithErr(c.Tr("user.form.name_reserved", err.(models.ErrNameReserved).Name), SIGNUP, &f) + case models.IsErrNamePatternNotAllowed(err): + c.Data["Err_UserName"] = true + c.RenderWithErr(c.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), SIGNUP, &f) + default: + c.Handle(500, "CreateUser", err) + } + return + } + log.Trace("Account created: %s", u.Name) + + // Auto-set admin for the only user. + if models.CountUsers() == 1 { + u.IsAdmin = true + u.IsActive = true + if err := models.UpdateUser(u); err != nil { + c.Handle(500, "UpdateUser", err) + return + } + } + + // Send confirmation email, no need for social account. + if setting.Service.RegisterEmailConfirm && u.ID > 1 { + mailer.SendActivateAccountMail(c.Context, models.NewMailerUser(u)) + c.Data["IsSendRegisterMail"] = true + c.Data["Email"] = u.Email + c.Data["Hours"] = setting.Service.ActiveCodeLives / 60 + c.HTML(200, ACTIVATE) + + if err := c.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { + log.Error(4, "Set cache(MailResendLimit) fail: %v", err) + } + return + } + + c.Redirect(setting.AppSubURL + "/user/login") +} + +func Activate(c *context.Context) { + code := c.Query("code") + if len(code) == 0 { + c.Data["IsActivatePage"] = true + if c.User.IsActive { + c.Error(404) + return + } + // Resend confirmation email. + if setting.Service.RegisterEmailConfirm { + if c.Cache.IsExist("MailResendLimit_" + c.User.LowerName) { + c.Data["ResendLimited"] = true + } else { + c.Data["Hours"] = setting.Service.ActiveCodeLives / 60 + mailer.SendActivateAccountMail(c.Context, models.NewMailerUser(c.User)) + + keyName := "MailResendLimit_" + c.User.LowerName + if err := c.Cache.Put(keyName, c.User.LowerName, 180); err != nil { + log.Error(2, "Set cache '%s' fail: %v", keyName, err) + } + } + } else { + c.Data["ServiceNotEnabled"] = true + } + c.HTML(200, ACTIVATE) + return + } + + // Verify code. + if user := models.VerifyUserActiveCode(code); user != nil { + user.IsActive = true + var err error + if user.Rands, err = models.GetUserSalt(); err != nil { + c.Handle(500, "UpdateUser", err) + return + } + if err := models.UpdateUser(user); err != nil { + c.Handle(500, "UpdateUser", err) + return + } + + log.Trace("User activated: %s", user.Name) + + c.Session.Set("uid", user.ID) + c.Session.Set("uname", user.Name) + c.Redirect(setting.AppSubURL + "/") + return + } + + c.Data["IsActivateFailed"] = true + c.HTML(200, ACTIVATE) +} + +func ActivateEmail(c *context.Context) { + code := c.Query("code") + email_string := c.Query("email") + + // Verify code. + if email := models.VerifyActiveEmailCode(code, email_string); email != nil { + if err := email.Activate(); err != nil { + c.Handle(500, "ActivateEmail", err) + } + + log.Trace("Email activated: %s", email.Email) + c.Flash.Success(c.Tr("settings.add_email_success")) + } + + c.Redirect(setting.AppSubURL + "/user/settings/email") + return +} + +func ForgotPasswd(c *context.Context) { + c.Data["Title"] = c.Tr("auth.forgot_password") + + if setting.MailService == nil { + c.Data["IsResetDisable"] = true + c.HTML(200, FORGOT_PASSWORD) + return + } + + c.Data["IsResetRequest"] = true + c.HTML(200, FORGOT_PASSWORD) +} + +func ForgotPasswdPost(c *context.Context) { + c.Data["Title"] = c.Tr("auth.forgot_password") + + if setting.MailService == nil { + c.Handle(403, "ForgotPasswdPost", nil) + return + } + c.Data["IsResetRequest"] = true + + email := c.Query("email") + c.Data["Email"] = email + + u, err := models.GetUserByEmail(email) + if err != nil { + if errors.IsUserNotExist(err) { + c.Data["Hours"] = setting.Service.ActiveCodeLives / 60 + c.Data["IsResetSent"] = true + c.HTML(200, FORGOT_PASSWORD) + return + } else { + c.Handle(500, "user.ResetPasswd(check existence)", err) + } + return + } + + if !u.IsLocal() { + c.Data["Err_Email"] = true + c.RenderWithErr(c.Tr("auth.non_local_account"), FORGOT_PASSWORD, nil) + return + } + + if c.Cache.IsExist("MailResendLimit_" + u.LowerName) { + c.Data["ResendLimited"] = true + c.HTML(200, FORGOT_PASSWORD) + return + } + + mailer.SendResetPasswordMail(c.Context, models.NewMailerUser(u)) + if err = c.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { + log.Error(4, "Set cache(MailResendLimit) fail: %v", err) + } + + c.Data["Hours"] = setting.Service.ActiveCodeLives / 60 + c.Data["IsResetSent"] = true + c.HTML(200, FORGOT_PASSWORD) +} + +func ResetPasswd(c *context.Context) { + c.Data["Title"] = c.Tr("auth.reset_password") + + code := c.Query("code") + if len(code) == 0 { + c.Error(404) + return + } + c.Data["Code"] = code + c.Data["IsResetForm"] = true + c.HTML(200, RESET_PASSWORD) +} + +func ResetPasswdPost(c *context.Context) { + c.Data["Title"] = c.Tr("auth.reset_password") + + code := c.Query("code") + if len(code) == 0 { + c.Error(404) + return + } + c.Data["Code"] = code + + if u := models.VerifyUserActiveCode(code); u != nil { + // Validate password length. + passwd := c.Query("password") + if len(passwd) < 6 { + c.Data["IsResetForm"] = true + c.Data["Err_Password"] = true + c.RenderWithErr(c.Tr("auth.password_too_short"), RESET_PASSWORD, nil) + return + } + + u.Passwd = passwd + var err error + if u.Rands, err = models.GetUserSalt(); err != nil { + c.Handle(500, "UpdateUser", err) + return + } + if u.Salt, err = models.GetUserSalt(); err != nil { + c.Handle(500, "UpdateUser", err) + return + } + u.EncodePasswd() + if err := models.UpdateUser(u); err != nil { + c.Handle(500, "UpdateUser", err) + return + } + + log.Trace("User password reset: %s", u.Name) + c.Redirect(setting.AppSubURL + "/user/login") + return + } + + c.Data["IsResetFailed"] = true + c.HTML(200, RESET_PASSWORD) +} diff --git a/routes/user/home.go b/routes/user/home.go new file mode 100644 index 00000000..c3b9b182 --- /dev/null +++ b/routes/user/home.go @@ -0,0 +1,424 @@ +// 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 ( + "bytes" + "fmt" + + "github.com/Unknwon/com" + "github.com/Unknwon/paginater" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" +) + +const ( + DASHBOARD = "user/dashboard/dashboard" + NEWS_FEED = "user/dashboard/feeds" + ISSUES = "user/dashboard/issues" + PROFILE = "user/profile" + ORG_HOME = "org/home" +) + +// getDashboardContextUser finds out dashboard is viewing as which context user. +func getDashboardContextUser(c *context.Context) *models.User { + ctxUser := c.User + orgName := c.Params(":org") + if len(orgName) > 0 { + // Organization. + org, err := models.GetUserByName(orgName) + if err != nil { + c.NotFoundOrServerError("GetUserByName", errors.IsUserNotExist, err) + return nil + } + ctxUser = org + } + c.Data["ContextUser"] = ctxUser + + if err := c.User.GetOrganizations(true); err != nil { + c.Handle(500, "GetOrganizations", err) + return nil + } + c.Data["Orgs"] = c.User.Orgs + + return ctxUser +} + +// retrieveFeeds loads feeds from database by given context user. +// The user could be organization so it is not always the logged in user, +// which is why we have to explicitly pass the context user ID. +func retrieveFeeds(c *context.Context, ctxUser *models.User, userID int64, isProfile bool) { + actions, err := models.GetFeeds(ctxUser, userID, c.QueryInt64("after_id"), isProfile) + if err != nil { + c.Handle(500, "GetFeeds", err) + return + } + + // Check access of private repositories. + feeds := make([]*models.Action, 0, len(actions)) + unameAvatars := make(map[string]string) + for _, act := range actions { + // Cache results to reduce queries. + _, ok := unameAvatars[act.ActUserName] + if !ok { + u, err := models.GetUserByName(act.ActUserName) + if err != nil { + if errors.IsUserNotExist(err) { + continue + } + c.Handle(500, "GetUserByName", err) + return + } + unameAvatars[act.ActUserName] = u.RelAvatarLink() + } + + act.ActAvatar = unameAvatars[act.ActUserName] + feeds = append(feeds, act) + } + c.Data["Feeds"] = feeds + if len(feeds) > 0 { + afterID := feeds[len(feeds)-1].ID + c.Data["AfterID"] = afterID + c.Header().Set("X-AJAX-URL", fmt.Sprintf("%s?after_id=%d", c.Data["Link"], afterID)) + } +} + +func Dashboard(c *context.Context) { + ctxUser := getDashboardContextUser(c) + if c.Written() { + return + } + + retrieveFeeds(c, ctxUser, c.User.ID, false) + if c.Written() { + return + } + + if c.Req.Header.Get("X-AJAX") == "true" { + c.HTML(200, NEWS_FEED) + return + } + + c.Data["Title"] = ctxUser.DisplayName() + " - " + c.Tr("dashboard") + c.Data["PageIsDashboard"] = true + c.Data["PageIsNews"] = true + + // Only user can have collaborative repositories. + if !ctxUser.IsOrganization() { + collaborateRepos, err := c.User.GetAccessibleRepositories(setting.UI.User.RepoPagingNum) + if err != nil { + c.Handle(500, "GetAccessibleRepositories", err) + return + } else if err = models.RepositoryList(collaborateRepos).LoadAttributes(); err != nil { + c.Handle(500, "RepositoryList.LoadAttributes", err) + return + } + c.Data["CollaborativeRepos"] = collaborateRepos + } + + var err error + var repos, mirrors []*models.Repository + var repoCount int64 + if ctxUser.IsOrganization() { + repos, repoCount, err = ctxUser.GetUserRepositories(c.User.ID, 1, setting.UI.User.RepoPagingNum) + if err != nil { + c.Handle(500, "GetUserRepositories", err) + return + } + + mirrors, err = ctxUser.GetUserMirrorRepositories(c.User.ID) + if err != nil { + c.Handle(500, "GetUserMirrorRepositories", err) + return + } + } else { + if err = ctxUser.GetRepositories(1, setting.UI.User.RepoPagingNum); err != nil { + c.Handle(500, "GetRepositories", err) + return + } + repos = ctxUser.Repos + repoCount = int64(ctxUser.NumRepos) + + mirrors, err = ctxUser.GetMirrorRepositories() + if err != nil { + c.Handle(500, "GetMirrorRepositories", err) + return + } + } + c.Data["Repos"] = repos + c.Data["RepoCount"] = repoCount + c.Data["MaxShowRepoNum"] = setting.UI.User.RepoPagingNum + + if err := models.MirrorRepositoryList(mirrors).LoadAttributes(); err != nil { + c.Handle(500, "MirrorRepositoryList.LoadAttributes", err) + return + } + c.Data["MirrorCount"] = len(mirrors) + c.Data["Mirrors"] = mirrors + + c.HTML(200, DASHBOARD) +} + +func Issues(c *context.Context) { + isPullList := c.Params(":type") == "pulls" + if isPullList { + c.Data["Title"] = c.Tr("pull_requests") + c.Data["PageIsPulls"] = true + } else { + c.Data["Title"] = c.Tr("issues") + c.Data["PageIsIssues"] = true + } + + ctxUser := getDashboardContextUser(c) + if c.Written() { + return + } + + var ( + sortType = c.Query("sort") + filterMode = models.FILTER_MODE_YOUR_REPOS + ) + + // Note: Organization does not have view type and filter mode. + if !ctxUser.IsOrganization() { + viewType := c.Query("type") + types := []string{ + string(models.FILTER_MODE_YOUR_REPOS), + string(models.FILTER_MODE_ASSIGN), + string(models.FILTER_MODE_CREATE), + } + if !com.IsSliceContainsStr(types, viewType) { + viewType = string(models.FILTER_MODE_YOUR_REPOS) + } + filterMode = models.FilterMode(viewType) + } + + page := c.QueryInt("page") + if page <= 1 { + page = 1 + } + + repoID := c.QueryInt64("repo") + isShowClosed := c.Query("state") == "closed" + + // Get repositories. + var ( + err error + repos []*models.Repository + userRepoIDs []int64 + showRepos = make([]*models.Repository, 0, 10) + ) + if ctxUser.IsOrganization() { + repos, _, err = ctxUser.GetUserRepositories(c.User.ID, 1, ctxUser.NumRepos) + if err != nil { + c.Handle(500, "GetRepositories", err) + return + } + } else { + if err := ctxUser.GetRepositories(1, c.User.NumRepos); err != nil { + c.Handle(500, "GetRepositories", err) + return + } + repos = ctxUser.Repos + } + + userRepoIDs = make([]int64, 0, len(repos)) + for _, repo := range repos { + userRepoIDs = append(userRepoIDs, repo.ID) + + if filterMode != models.FILTER_MODE_YOUR_REPOS { + continue + } + + if isPullList { + if isShowClosed && repo.NumClosedPulls == 0 || + !isShowClosed && repo.NumOpenPulls == 0 { + continue + } + } else { + if !repo.EnableIssues || repo.EnableExternalTracker || + isShowClosed && repo.NumClosedIssues == 0 || + !isShowClosed && repo.NumOpenIssues == 0 { + continue + } + } + + showRepos = append(showRepos, repo) + } + + // Filter repositories if the page shows issues. + if !isPullList { + userRepoIDs, err = models.FilterRepositoryWithIssues(userRepoIDs) + if err != nil { + c.Handle(500, "FilterRepositoryWithIssues", err) + return + } + } + + issueOptions := &models.IssuesOptions{ + RepoID: repoID, + Page: page, + IsClosed: isShowClosed, + IsPull: isPullList, + SortType: sortType, + } + switch filterMode { + case models.FILTER_MODE_YOUR_REPOS: + // Get all issues from repositories from this user. + if userRepoIDs == nil { + issueOptions.RepoIDs = []int64{-1} + } else { + issueOptions.RepoIDs = userRepoIDs + } + + case models.FILTER_MODE_ASSIGN: + // Get all issues assigned to this user. + issueOptions.AssigneeID = ctxUser.ID + + case models.FILTER_MODE_CREATE: + // Get all issues created by this user. + issueOptions.PosterID = ctxUser.ID + } + + issues, err := models.Issues(issueOptions) + if err != nil { + c.Handle(500, "Issues", err) + return + } + + if repoID > 0 { + repo, err := models.GetRepositoryByID(repoID) + if err != nil { + c.Handle(500, "GetRepositoryByID", fmt.Errorf("[#%d] %v", repoID, err)) + return + } + + if err = repo.GetOwner(); err != nil { + c.Handle(500, "GetOwner", fmt.Errorf("[#%d] %v", repoID, err)) + return + } + + // Check if user has access to given repository. + if !repo.IsOwnedBy(ctxUser.ID) && !repo.HasAccess(ctxUser.ID) { + c.Handle(404, "Issues", fmt.Errorf("#%d", repoID)) + return + } + } + + for _, issue := range issues { + if err = issue.Repo.GetOwner(); err != nil { + c.Handle(500, "GetOwner", fmt.Errorf("[#%d] %v", issue.RepoID, err)) + return + } + } + + issueStats := models.GetUserIssueStats(repoID, ctxUser.ID, userRepoIDs, filterMode, isPullList) + + var total int + if !isShowClosed { + total = int(issueStats.OpenCount) + } else { + total = int(issueStats.ClosedCount) + } + + c.Data["Issues"] = issues + c.Data["Repos"] = showRepos + c.Data["Page"] = paginater.New(total, setting.UI.IssuePagingNum, page, 5) + c.Data["IssueStats"] = issueStats + c.Data["ViewType"] = string(filterMode) + c.Data["SortType"] = sortType + c.Data["RepoID"] = repoID + c.Data["IsShowClosed"] = isShowClosed + + if isShowClosed { + c.Data["State"] = "closed" + } else { + c.Data["State"] = "open" + } + + c.HTML(200, ISSUES) +} + +func ShowSSHKeys(c *context.Context, uid int64) { + keys, err := models.ListPublicKeys(uid) + if err != nil { + c.Handle(500, "ListPublicKeys", err) + return + } + + var buf bytes.Buffer + for i := range keys { + buf.WriteString(keys[i].OmitEmail()) + buf.WriteString("\n") + } + c.PlainText(200, buf.Bytes()) +} + +func showOrgProfile(c *context.Context) { + c.SetParams(":org", c.Params(":username")) + context.HandleOrgAssignment(c) + if c.Written() { + return + } + + org := c.Org.Organization + c.Data["Title"] = org.FullName + + page := c.QueryInt("page") + if page <= 0 { + page = 1 + } + + var ( + repos []*models.Repository + count int64 + err error + ) + if c.IsLogged && !c.User.IsAdmin { + repos, count, err = org.GetUserRepositories(c.User.ID, page, setting.UI.User.RepoPagingNum) + if err != nil { + c.Handle(500, "GetUserRepositories", err) + return + } + c.Data["Repos"] = repos + } else { + showPrivate := c.IsLogged && c.User.IsAdmin + repos, err = models.GetUserRepositories(&models.UserRepoOptions{ + UserID: org.ID, + Private: showPrivate, + Page: page, + PageSize: setting.UI.User.RepoPagingNum, + }) + if err != nil { + c.Handle(500, "GetRepositories", err) + return + } + c.Data["Repos"] = repos + count = models.CountUserRepositories(org.ID, showPrivate) + } + c.Data["Page"] = paginater.New(int(count), setting.UI.User.RepoPagingNum, page, 5) + + if err := org.GetMembers(); err != nil { + c.Handle(500, "GetMembers", err) + return + } + c.Data["Members"] = org.Members + + c.Data["Teams"] = org.Teams + + c.HTML(200, ORG_HOME) +} + +func Email2User(c *context.Context) { + u, err := models.GetUserByEmail(c.Query("email")) + if err != nil { + c.NotFoundOrServerError("GetUserByEmail", errors.IsUserNotExist, err) + return + } + c.Redirect(setting.AppSubURL + "/user/" + u.Name) +} diff --git a/routes/user/profile.go b/routes/user/profile.go new file mode 100644 index 00000000..a6eba351 --- /dev/null +++ b/routes/user/profile.go @@ -0,0 +1,169 @@ +// 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 ( + "fmt" + "path" + "strings" + + "github.com/Unknwon/paginater" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/routes/repo" +) + +const ( + FOLLOWERS = "user/meta/followers" + STARS = "user/meta/stars" +) + +func GetUserByName(c *context.Context, name string) *models.User { + user, err := models.GetUserByName(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.Context) *models.User { + return GetUserByName(c, c.Params(":username")) +} + +func Profile(c *context.Context) { + uname := c.Params(":username") + // Special handle for FireFox requests favicon.ico. + if uname == "favicon.ico" { + c.ServeFile(path.Join(setting.StaticRootPath, "public/img/favicon.png")) + return + } else if strings.HasSuffix(uname, ".png") { + c.Error(404) + return + } + + isShowKeys := false + if strings.HasSuffix(uname, ".keys") { + isShowKeys = true + } + + ctxUser := GetUserByName(c, strings.TrimSuffix(uname, ".keys")) + if c.Written() { + return + } + + // Show SSH keys. + if isShowKeys { + ShowSSHKeys(c, ctxUser.ID) + return + } + + if ctxUser.IsOrganization() { + showOrgProfile(c) + return + } + + c.Data["Title"] = ctxUser.DisplayName() + c.Data["PageIsUserProfile"] = true + c.Data["Owner"] = ctxUser + + orgs, err := models.GetOrgsByUserID(ctxUser.ID, c.IsLogged && (c.User.IsAdmin || c.User.ID == ctxUser.ID)) + if err != nil { + c.Handle(500, "GetOrgsByUserIDDesc", err) + return + } + + c.Data["Orgs"] = orgs + + tab := c.Query("tab") + c.Data["TabName"] = tab + switch tab { + case "activity": + retrieveFeeds(c, ctxUser, -1, true) + if c.Written() { + return + } + default: + page := c.QueryInt("page") + if page <= 0 { + page = 1 + } + + showPrivate := c.IsLogged && (ctxUser.ID == c.User.ID || c.User.IsAdmin) + c.Data["Repos"], err = models.GetUserRepositories(&models.UserRepoOptions{ + UserID: ctxUser.ID, + Private: showPrivate, + Page: page, + PageSize: setting.UI.User.RepoPagingNum, + }) + if err != nil { + c.Handle(500, "GetRepositories", err) + return + } + + count := models.CountUserRepositories(ctxUser.ID, showPrivate) + c.Data["Page"] = paginater.New(int(count), setting.UI.User.RepoPagingNum, page, 5) + } + + c.HTML(200, PROFILE) +} + +func Followers(c *context.Context) { + u := GetUserByParams(c) + if c.Written() { + return + } + c.Data["Title"] = u.DisplayName() + c.Data["CardsTitle"] = c.Tr("user.followers") + c.Data["PageIsFollowers"] = true + c.Data["Owner"] = u + repo.RenderUserCards(c, u.NumFollowers, u.GetFollowers, FOLLOWERS) +} + +func Following(c *context.Context) { + u := GetUserByParams(c) + if c.Written() { + return + } + c.Data["Title"] = u.DisplayName() + c.Data["CardsTitle"] = c.Tr("user.following") + c.Data["PageIsFollowing"] = true + c.Data["Owner"] = u + repo.RenderUserCards(c, u.NumFollowing, u.GetFollowing, FOLLOWERS) +} + +func Stars(c *context.Context) { + +} + +func Action(c *context.Context) { + u := GetUserByParams(c) + if c.Written() { + return + } + + var err error + switch c.Params(":action") { + case "follow": + err = models.FollowUser(c.User.ID, u.ID) + case "unfollow": + err = models.UnfollowUser(c.User.ID, u.ID) + } + + if err != nil { + c.Handle(500, fmt.Sprintf("Action (%s)", c.Params(":action")), err) + return + } + + redirectTo := c.Query("redirect_to") + if len(redirectTo) == 0 { + redirectTo = u.HomeLink() + } + c.Redirect(redirectTo) +} diff --git a/routes/user/setting.go b/routes/user/setting.go new file mode 100644 index 00000000..723b3da2 --- /dev/null +++ b/routes/user/setting.go @@ -0,0 +1,664 @@ +// 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 ( + "bytes" + "encoding/base64" + "fmt" + "html/template" + "image/png" + "io/ioutil" + "strings" + + "github.com/Unknwon/com" + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" + log "gopkg.in/clog.v1" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/models/errors" + "github.com/gogits/gogs/pkg/context" + "github.com/gogits/gogs/pkg/form" + "github.com/gogits/gogs/pkg/mailer" + "github.com/gogits/gogs/pkg/setting" + "github.com/gogits/gogs/pkg/tool" +) + +const ( + SETTINGS_PROFILE = "user/settings/profile" + SETTINGS_AVATAR = "user/settings/avatar" + SETTINGS_PASSWORD = "user/settings/password" + SETTINGS_EMAILS = "user/settings/email" + SETTINGS_SSH_KEYS = "user/settings/sshkeys" + SETTINGS_SECURITY = "user/settings/security" + SETTINGS_TWO_FACTOR_ENABLE = "user/settings/two_factor_enable" + SETTINGS_TWO_FACTOR_RECOVERY_CODES = "user/settings/two_factor_recovery_codes" + SETTINGS_REPOSITORIES = "user/settings/repositories" + SETTINGS_ORGANIZATIONS = "user/settings/organizations" + SETTINGS_APPLICATIONS = "user/settings/applications" + SETTINGS_DELETE = "user/settings/delete" + NOTIFICATION = "user/notification" +) + +func Settings(c *context.Context) { + c.Title("settings.profile") + c.PageIs("SettingsProfile") + c.Data["origin_name"] = c.User.Name + c.Data["name"] = c.User.Name + c.Data["full_name"] = c.User.FullName + c.Data["email"] = c.User.Email + c.Data["website"] = c.User.Website + c.Data["location"] = c.User.Location + c.Success(SETTINGS_PROFILE) +} + +func SettingsPost(c *context.Context, f form.UpdateProfile) { + c.Title("settings.profile") + c.PageIs("SettingsProfile") + c.Data["origin_name"] = c.User.Name + + if c.HasError() { + c.Success(SETTINGS_PROFILE) + return + } + + // Non-local users are not allowed to change their username + if c.User.IsLocal() { + // Check if username characters have been changed + if c.User.LowerName != strings.ToLower(f.Name) { + if err := models.ChangeUserName(c.User, f.Name); err != nil { + c.FormErr("Name") + var msg string + switch { + case models.IsErrUserAlreadyExist(err): + msg = c.Tr("form.username_been_taken") + case models.IsErrEmailAlreadyUsed(err): + msg = c.Tr("form.email_been_used") + case models.IsErrNameReserved(err): + msg = c.Tr("form.name_reserved") + case models.IsErrNamePatternNotAllowed(err): + msg = c.Tr("form.name_pattern_not_allowed") + default: + c.ServerError("ChangeUserName", err) + return + } + + c.RenderWithErr(msg, SETTINGS_PROFILE, &f) + return + } + + log.Trace("Username changed: %s -> %s", c.User.Name, f.Name) + } + + // In case it's just a case change + c.User.Name = f.Name + c.User.LowerName = strings.ToLower(f.Name) + } + + c.User.FullName = f.FullName + c.User.Email = f.Email + c.User.Website = f.Website + c.User.Location = f.Location + if err := models.UpdateUser(c.User); err != nil { + c.ServerError("UpdateUser", err) + return + } + + c.Flash.Success(c.Tr("settings.update_profile_success")) + c.SubURLRedirect("/user/settings") +} + +// FIXME: limit size. +func UpdateAvatarSetting(c *context.Context, f form.Avatar, ctxUser *models.User) error { + ctxUser.UseCustomAvatar = f.Source == form.AVATAR_LOCAL + if len(f.Gravatar) > 0 { + ctxUser.Avatar = tool.MD5(f.Gravatar) + ctxUser.AvatarEmail = f.Gravatar + } + + if f.Avatar != nil { + r, err := f.Avatar.Open() + if err != nil { + return fmt.Errorf("Avatar.Open: %v", err) + } + defer r.Close() + + data, err := ioutil.ReadAll(r) + if err != nil { + return fmt.Errorf("ioutil.ReadAll: %v", err) + } + if !tool.IsImageFile(data) { + return errors.New(c.Tr("settings.uploaded_avatar_not_a_image")) + } + if err = ctxUser.UploadAvatar(data); err != nil { + return fmt.Errorf("UploadAvatar: %v", err) + } + } else { + // No avatar is uploaded but setting has been changed to enable, + // generate a random one when needed. + if ctxUser.UseCustomAvatar && !com.IsFile(ctxUser.CustomAvatarPath()) { + if err := ctxUser.GenerateRandomAvatar(); err != nil { + log.Error(4, "GenerateRandomAvatar[%d]: %v", ctxUser.ID, err) + } + } + } + + if err := models.UpdateUser(ctxUser); err != nil { + return fmt.Errorf("UpdateUser: %v", err) + } + + return nil +} + +func SettingsAvatar(c *context.Context) { + c.Title("settings.avatar") + c.PageIs("SettingsAvatar") + c.Success(SETTINGS_AVATAR) +} + +func SettingsAvatarPost(c *context.Context, f form.Avatar) { + if err := UpdateAvatarSetting(c, f, c.User); err != nil { + c.Flash.Error(err.Error()) + } else { + c.Flash.Success(c.Tr("settings.update_avatar_success")) + } + + c.SubURLRedirect("/user/settings/avatar") +} + +func SettingsDeleteAvatar(c *context.Context) { + if err := c.User.DeleteAvatar(); err != nil { + c.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err)) + } + + c.SubURLRedirect("/user/settings/avatar") +} + +func SettingsPassword(c *context.Context) { + c.Title("settings.password") + c.PageIs("SettingsPassword") + c.Success(SETTINGS_PASSWORD) +} + +func SettingsPasswordPost(c *context.Context, f form.ChangePassword) { + c.Title("settings.password") + c.PageIs("SettingsPassword") + + if c.HasError() { + c.Success(SETTINGS_PASSWORD) + return + } + + if !c.User.ValidatePassword(f.OldPassword) { + c.Flash.Error(c.Tr("settings.password_incorrect")) + } else if f.Password != f.Retype { + c.Flash.Error(c.Tr("form.password_not_match")) + } else { + c.User.Passwd = f.Password + var err error + if c.User.Salt, err = models.GetUserSalt(); err != nil { + c.ServerError("GetUserSalt", err) + return + } + c.User.EncodePasswd() + if err := models.UpdateUser(c.User); err != nil { + c.ServerError("UpdateUser", err) + return + } + c.Flash.Success(c.Tr("settings.change_password_success")) + } + + c.SubURLRedirect("/user/settings/password") +} + +func SettingsEmails(c *context.Context) { + c.Title("settings.emails") + c.PageIs("SettingsEmails") + + emails, err := models.GetEmailAddresses(c.User.ID) + if err != nil { + c.ServerError("GetEmailAddresses", err) + return + } + c.Data["Emails"] = emails + + c.Success(SETTINGS_EMAILS) +} + +func SettingsEmailPost(c *context.Context, f form.AddEmail) { + c.Title("settings.emails") + c.PageIs("SettingsEmails") + + // Make emailaddress primary. + if c.Query("_method") == "PRIMARY" { + if err := models.MakeEmailPrimary(&models.EmailAddress{ID: c.QueryInt64("id")}); err != nil { + c.ServerError("MakeEmailPrimary", err) + return + } + + c.SubURLRedirect("/user/settings/email") + return + } + + // Add Email address. + emails, err := models.GetEmailAddresses(c.User.ID) + if err != nil { + c.ServerError("GetEmailAddresses", err) + return + } + c.Data["Emails"] = emails + + if c.HasError() { + c.Success(SETTINGS_EMAILS) + return + } + + email := &models.EmailAddress{ + UID: c.User.ID, + Email: f.Email, + IsActivated: !setting.Service.RegisterEmailConfirm, + } + if err := models.AddEmailAddress(email); err != nil { + if models.IsErrEmailAlreadyUsed(err) { + c.RenderWithErr(c.Tr("form.email_been_used"), SETTINGS_EMAILS, &f) + } else { + c.ServerError("AddEmailAddress", err) + } + return + } + + // Send confirmation email + if setting.Service.RegisterEmailConfirm { + mailer.SendActivateEmailMail(c.Context, models.NewMailerUser(c.User), email.Email) + + if err := c.Cache.Put("MailResendLimit_"+c.User.LowerName, c.User.LowerName, 180); err != nil { + log.Error(2, "Set cache 'MailResendLimit' failed: %v", err) + } + c.Flash.Info(c.Tr("settings.add_email_confirmation_sent", email.Email, setting.Service.ActiveCodeLives/60)) + } else { + c.Flash.Success(c.Tr("settings.add_email_success")) + } + + c.SubURLRedirect("/user/settings/email") +} + +func DeleteEmail(c *context.Context) { + if err := models.DeleteEmailAddress(&models.EmailAddress{ + ID: c.QueryInt64("id"), + UID: c.User.ID, + }); err != nil { + c.ServerError("DeleteEmailAddress", err) + return + } + + c.Flash.Success(c.Tr("settings.email_deletion_success")) + c.JSONSuccess(map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/email", + }) +} + +func SettingsSSHKeys(c *context.Context) { + c.Title("settings.ssh_keys") + c.PageIs("SettingsSSHKeys") + + keys, err := models.ListPublicKeys(c.User.ID) + if err != nil { + c.ServerError("ListPublicKeys", err) + return + } + c.Data["Keys"] = keys + + c.Success(SETTINGS_SSH_KEYS) +} + +func SettingsSSHKeysPost(c *context.Context, f form.AddSSHKey) { + c.Title("settings.ssh_keys") + c.PageIs("SettingsSSHKeys") + + keys, err := models.ListPublicKeys(c.User.ID) + if err != nil { + c.ServerError("ListPublicKeys", err) + return + } + c.Data["Keys"] = keys + + if c.HasError() { + c.Success(SETTINGS_SSH_KEYS) + return + } + + content, err := models.CheckPublicKeyString(f.Content) + if err != nil { + if models.IsErrKeyUnableVerify(err) { + c.Flash.Info(c.Tr("form.unable_verify_ssh_key")) + } else { + c.Flash.Error(c.Tr("form.invalid_ssh_key", err.Error())) + c.SubURLRedirect("/user/settings/ssh") + return + } + } + + if _, err = models.AddPublicKey(c.User.ID, f.Title, content); err != nil { + c.Data["HasError"] = true + switch { + case models.IsErrKeyAlreadyExist(err): + c.FormErr("Content") + c.RenderWithErr(c.Tr("settings.ssh_key_been_used"), SETTINGS_SSH_KEYS, &f) + case models.IsErrKeyNameAlreadyUsed(err): + c.FormErr("Title") + c.RenderWithErr(c.Tr("settings.ssh_key_name_used"), SETTINGS_SSH_KEYS, &f) + default: + c.ServerError("AddPublicKey", err) + } + return + } + + c.Flash.Success(c.Tr("settings.add_key_success", f.Title)) + c.SubURLRedirect("/user/settings/ssh") +} + +func DeleteSSHKey(c *context.Context) { + if err := models.DeletePublicKey(c.User, c.QueryInt64("id")); err != nil { + c.Flash.Error("DeletePublicKey: " + err.Error()) + } else { + c.Flash.Success(c.Tr("settings.ssh_key_deletion_success")) + } + + c.JSONSuccess(map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/ssh", + }) +} + +func SettingsSecurity(c *context.Context) { + c.Title("settings.security") + c.PageIs("SettingsSecurity") + + t, err := models.GetTwoFactorByUserID(c.UserID()) + if err != nil && !errors.IsTwoFactorNotFound(err) { + c.ServerError("GetTwoFactorByUserID", err) + return + } + c.Data["TwoFactor"] = t + + c.Success(SETTINGS_SECURITY) +} + +func SettingsTwoFactorEnable(c *context.Context) { + if c.User.IsEnabledTwoFactor() { + c.NotFound() + return + } + + c.Title("settings.two_factor_enable_title") + c.PageIs("SettingsSecurity") + + var key *otp.Key + var err error + keyURL := c.Session.Get("twoFactorURL") + if keyURL != nil { + key, _ = otp.NewKeyFromURL(keyURL.(string)) + } + if key == nil { + key, err = totp.Generate(totp.GenerateOpts{ + Issuer: setting.AppName, + AccountName: c.User.Email, + }) + if err != nil { + c.ServerError("Generate", err) + return + } + } + c.Data["TwoFactorSecret"] = key.Secret() + + img, err := key.Image(240, 240) + if err != nil { + c.ServerError("Image", err) + return + } + + var buf bytes.Buffer + if err = png.Encode(&buf, img); err != nil { + c.ServerError("Encode", err) + return + } + c.Data["QRCode"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())) + + c.Session.Set("twoFactorSecret", c.Data["TwoFactorSecret"]) + c.Session.Set("twoFactorURL", key.String()) + c.Success(SETTINGS_TWO_FACTOR_ENABLE) +} + +func SettingsTwoFactorEnablePost(c *context.Context) { + secret, ok := c.Session.Get("twoFactorSecret").(string) + if !ok { + c.NotFound() + return + } + + if !totp.Validate(c.Query("passcode"), secret) { + c.Flash.Error(c.Tr("settings.two_factor_invalid_passcode")) + c.SubURLRedirect("/user/settings/security/two_factor_enable") + return + } + + if err := models.NewTwoFactor(c.UserID(), secret); err != nil { + c.Flash.Error(c.Tr("settings.two_factor_enable_error", err)) + c.SubURLRedirect("/user/settings/security/two_factor_enable") + return + } + + c.Session.Delete("twoFactorSecret") + c.Session.Delete("twoFactorURL") + c.Flash.Success(c.Tr("settings.two_factor_enable_success")) + c.SubURLRedirect("/user/settings/security/two_factor_recovery_codes") +} + +func SettingsTwoFactorRecoveryCodes(c *context.Context) { + if !c.User.IsEnabledTwoFactor() { + c.NotFound() + return + } + + c.Title("settings.two_factor_recovery_codes_title") + c.PageIs("SettingsSecurity") + + recoveryCodes, err := models.GetRecoveryCodesByUserID(c.UserID()) + if err != nil { + c.ServerError("GetRecoveryCodesByUserID", err) + return + } + c.Data["RecoveryCodes"] = recoveryCodes + + c.Success(SETTINGS_TWO_FACTOR_RECOVERY_CODES) +} + +func SettingsTwoFactorRecoveryCodesPost(c *context.Context) { + if !c.User.IsEnabledTwoFactor() { + c.NotFound() + return + } + + if err := models.RegenerateRecoveryCodes(c.UserID()); err != nil { + c.Flash.Error(c.Tr("settings.two_factor_regenerate_recovery_codes_error", err)) + } else { + c.Flash.Success(c.Tr("settings.two_factor_regenerate_recovery_codes_success")) + } + + c.SubURLRedirect("/user/settings/security/two_factor_recovery_codes") +} + +func SettingsTwoFactorDisable(c *context.Context) { + if !c.User.IsEnabledTwoFactor() { + c.NotFound() + return + } + + if err := models.DeleteTwoFactor(c.UserID()); err != nil { + c.ServerError("DeleteTwoFactor", err) + return + } + + c.Flash.Success(c.Tr("settings.two_factor_disable_success")) + c.JSONSuccess(map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/security", + }) +} + +func SettingsRepos(c *context.Context) { + c.Title("settings.repos") + c.PageIs("SettingsRepositories") + + repos, err := models.GetUserAndCollaborativeRepositories(c.User.ID) + if err != nil { + c.ServerError("GetUserAndCollaborativeRepositories", err) + return + } + if err = models.RepositoryList(repos).LoadAttributes(); err != nil { + c.ServerError("LoadAttributes", err) + return + } + c.Data["Repos"] = repos + + c.Success(SETTINGS_REPOSITORIES) +} + +func SettingsLeaveRepo(c *context.Context) { + repo, err := models.GetRepositoryByID(c.QueryInt64("id")) + if err != nil { + c.NotFoundOrServerError("GetRepositoryByID", errors.IsRepoNotExist, err) + return + } + + if err = repo.DeleteCollaboration(c.User.ID); err != nil { + c.ServerError("DeleteCollaboration", err) + return + } + + c.Flash.Success(c.Tr("settings.repos.leave_success", repo.FullName())) + c.JSONSuccess(map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/repositories", + }) +} + +func SettingsOrganizations(c *context.Context) { + c.Title("settings.orgs") + c.PageIs("SettingsOrganizations") + + orgs, err := models.GetOrgsByUserID(c.User.ID, true) + if err != nil { + c.ServerError("GetOrgsByUserID", err) + return + } + c.Data["Orgs"] = orgs + + c.Success(SETTINGS_ORGANIZATIONS) +} + +func SettingsLeaveOrganization(c *context.Context) { + if err := models.RemoveOrgUser(c.QueryInt64("id"), c.User.ID); err != nil { + if models.IsErrLastOrgOwner(err) { + c.Flash.Error(c.Tr("form.last_org_owner")) + } else { + c.ServerError("RemoveOrgUser", err) + return + } + } + + c.JSONSuccess(map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/organizations", + }) +} + +func SettingsApplications(c *context.Context) { + c.Title("settings.applications") + c.PageIs("SettingsApplications") + + tokens, err := models.ListAccessTokens(c.User.ID) + if err != nil { + c.ServerError("ListAccessTokens", err) + return + } + c.Data["Tokens"] = tokens + + c.Success(SETTINGS_APPLICATIONS) +} + +func SettingsApplicationsPost(c *context.Context, f form.NewAccessToken) { + c.Title("settings.applications") + c.PageIs("SettingsApplications") + + if c.HasError() { + tokens, err := models.ListAccessTokens(c.User.ID) + if err != nil { + c.ServerError("ListAccessTokens", err) + return + } + + c.Data["Tokens"] = tokens + c.Success(SETTINGS_APPLICATIONS) + return + } + + t := &models.AccessToken{ + UID: c.User.ID, + Name: f.Name, + } + if err := models.NewAccessToken(t); err != nil { + c.ServerError("NewAccessToken", err) + return + } + + c.Flash.Success(c.Tr("settings.generate_token_succees")) + c.Flash.Info(t.Sha1) + c.SubURLRedirect("/user/settings/applications") +} + +func SettingsDeleteApplication(c *context.Context) { + if err := models.DeleteAccessTokenOfUserByID(c.User.ID, c.QueryInt64("id")); err != nil { + c.Flash.Error("DeleteAccessTokenByID: " + err.Error()) + } else { + c.Flash.Success(c.Tr("settings.delete_token_success")) + } + + c.JSONSuccess(map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/applications", + }) +} + +func SettingsDelete(c *context.Context) { + c.Title("settings.delete") + c.PageIs("SettingsDelete") + + if c.Req.Method == "POST" { + if _, err := models.UserSignIn(c.User.Name, c.Query("password")); err != nil { + if errors.IsUserNotExist(err) { + c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SETTINGS_DELETE, nil) + } else { + c.ServerError("UserSignIn", err) + } + return + } + + if err := models.DeleteUser(c.User); err != nil { + switch { + case models.IsErrUserOwnRepos(err): + c.Flash.Error(c.Tr("form.still_own_repo")) + c.Redirect(setting.AppSubURL + "/user/settings/delete") + case models.IsErrUserHasOrgs(err): + c.Flash.Error(c.Tr("form.still_has_org")) + c.Redirect(setting.AppSubURL + "/user/settings/delete") + default: + c.ServerError("DeleteUser", err) + } + } else { + log.Trace("Account deleted: %s", c.User.Name) + c.Redirect(setting.AppSubURL + "/") + } + return + } + + c.Success(SETTINGS_DELETE) +} |