diff options
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | README_ZH.md | 3 | ||||
-rw-r--r-- | gogs.go | 2 | ||||
-rw-r--r-- | models/access.go | 6 | ||||
-rw-r--r-- | models/action.go | 21 | ||||
-rw-r--r-- | models/repo.go | 41 | ||||
-rw-r--r-- | models/user.go | 8 | ||||
-rw-r--r-- | modules/auth/auth.go | 6 | ||||
-rw-r--r-- | modules/auth/user.go | 1 | ||||
-rw-r--r-- | modules/base/base.go | 1 | ||||
-rw-r--r-- | modules/middleware/binding.go | 12 | ||||
-rw-r--r-- | modules/middleware/repo.go | 12 | ||||
-rwxr-xr-x | public/css/gogs.css | 75 | ||||
-rw-r--r-- | public/js/app.js | 44 | ||||
-rw-r--r-- | routers/repo/repo.go | 113 | ||||
-rw-r--r-- | routers/repo/setting.go | 206 | ||||
-rw-r--r-- | routers/user/setting.go | 1 | ||||
-rw-r--r-- | templates/base/navbar.tmpl | 2 | ||||
-rw-r--r-- | templates/repo/collaboration.tmpl | 45 | ||||
-rw-r--r-- | templates/repo/setting.tmpl | 13 | ||||
-rw-r--r-- | templates/repo/setting_nav.tmpl | 7 | ||||
-rw-r--r-- | templates/user/profile.tmpl | 1 | ||||
-rw-r--r-- | templates/user/setting.tmpl | 7 | ||||
-rw-r--r-- | web.go | 10 |
24 files changed, 470 insertions, 170 deletions
@@ -5,7 +5,7 @@ Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language  -##### Current version: 0.3.1 Alpha +##### Current version: 0.3.2 Alpha ### NOTICES @@ -36,6 +36,7 @@ More importantly, Gogs only needs one binary to setup your own project hosting o - Register/delete/rename account. - Create/migrate/mirror/delete/watch/rename/transfer public/private repository. - Repository viewer/release/issue tracker. +- Add/remove repository collaborators. - Gravatar and cache support. - Mail service(register, issue). - Administration panel. diff --git a/README_ZH.md b/README_ZH.md index e4c92685..a58bb1b5 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -5,7 +5,7 @@ Gogs(Go Git Service) 是一个由 Go 语言编写的自助 Git 托管服务。  -##### 当前版本:0.3.1 Alpha +##### 当前版本:0.3.2 Alpha ## 开发目的 @@ -27,6 +27,7 @@ Gogs 完全使用 Go 语言来实现对 Git 数据的操作,实现 **零** 依 - 注册/删除/重命名用户 - 创建/迁移/镜像/删除/关注/重命名/转移 公开/私有 仓库 - 仓库 浏览器/发布/缺陷追踪 +- 添加/删除 仓库协作者 - Gravatar 以及缓存支持 - 邮件服务(注册、Issue) - 管理员面板 @@ -19,7 +19,7 @@ import ( // Test that go1.2 tag above is included in builds. main.go refers to this definition. const go12tag = true -const APP_VER = "0.3.1.0501 Alpha" +const APP_VER = "0.3.2.0501 Alpha" func init() { base.AppVer = APP_VER diff --git a/models/access.go b/models/access.go index 970f4a94..749a2604 100644 --- a/models/access.go +++ b/models/access.go @@ -42,6 +42,12 @@ func UpdateAccess(access *Access) error { return err } +// DeleteAccess deletes access record. +func DeleteAccess(access *Access) error { + _, err := orm.Delete(access) + return err +} + // UpdateAccess updates access information with session for rolling back. func UpdateAccessWithSession(sess *xorm.Session, access *Access) error { if _, err := sess.Id(access.Id).Update(access); err != nil { diff --git a/models/action.go b/models/action.go index a9a41a9f..2fc81734 100644 --- a/models/action.go +++ b/models/action.go @@ -40,6 +40,7 @@ type Action struct { RepoId int64 RepoName string RefName string + IsPrivate bool `xorm:"not null"` Content string `xorm:"TEXT"` Created time.Time `xorm:"created"` } @@ -88,12 +89,6 @@ func CommitRepoAction(userId int64, userName, actEmail string, return err } - if err = NotifyWatchers(&Action{ActUserId: userId, ActUserName: userName, ActEmail: actEmail, - OpType: opType, Content: string(bs), RepoId: repoId, RepoName: repoName, RefName: refName}); err != nil { - log.Error("action.CommitRepoAction(notify watchers): %d/%s", userId, repoName) - return err - } - // Change repository bare status and update last updated time. repo, err := GetRepositoryByName(userId, repoName) if err != nil { @@ -106,6 +101,13 @@ func CommitRepoAction(userId int64, userName, actEmail string, return err } + if err = NotifyWatchers(&Action{ActUserId: userId, ActUserName: userName, ActEmail: actEmail, + OpType: opType, Content: string(bs), RepoId: repoId, RepoName: repoName, RefName: refName, + IsPrivate: repo.IsPrivate}); err != nil { + log.Error("action.CommitRepoAction(notify watchers): %d/%s", userId, repoName) + return err + } + log.Trace("action.CommitRepoAction(end): %d/%s", userId, repoName) return nil } @@ -113,7 +115,7 @@ func CommitRepoAction(userId int64, userName, actEmail string, // NewRepoAction adds new action for creating repository. func NewRepoAction(user *User, repo *Repository) (err error) { if err = NotifyWatchers(&Action{ActUserId: user.Id, ActUserName: user.Name, ActEmail: user.Email, - OpType: OP_CREATE_REPO, RepoId: repo.Id, RepoName: repo.Name}); err != nil { + OpType: OP_CREATE_REPO, RepoId: repo.Id, RepoName: repo.Name, IsPrivate: repo.IsPrivate}); err != nil { log.Error("action.NewRepoAction(notify watchers): %d/%s", user.Id, repo.Name) return err } @@ -125,7 +127,8 @@ func NewRepoAction(user *User, repo *Repository) (err error) { // TransferRepoAction adds new action for transfering repository. func TransferRepoAction(user, newUser *User, repo *Repository) (err error) { if err = NotifyWatchers(&Action{ActUserId: user.Id, ActUserName: user.Name, ActEmail: user.Email, - OpType: OP_TRANSFER_REPO, RepoId: repo.Id, RepoName: repo.Name, Content: newUser.Name}); err != nil { + OpType: OP_TRANSFER_REPO, RepoId: repo.Id, RepoName: repo.Name, Content: newUser.Name, + IsPrivate: repo.IsPrivate}); err != nil { log.Error("action.TransferRepoAction(notify watchers): %d/%s", user.Id, repo.Name) return err } @@ -139,7 +142,7 @@ func GetFeeds(userid, offset int64, isProfile bool) ([]Action, error) { actions := make([]Action, 0, 20) sess := orm.Limit(20, int(offset)).Desc("id").Where("user_id=?", userid) if isProfile { - sess.And("act_user_id=?", userid) + sess.Where("is_private=?", false).And("act_user_id=?", userid) } else { sess.And("act_user_id!=?", userid) } diff --git a/models/repo.go b/models/repo.go index 5f66bca8..35b3fde1 100644 --- a/models/repo.go +++ b/models/repo.go @@ -246,14 +246,17 @@ func CreateRepository(user *User, name, desc, lang, license string, private, mir } repo := &Repository{ - OwnerId: user.Id, - Name: name, - LowerName: strings.ToLower(name), - Description: desc, - IsPrivate: private, - IsBare: lang == "" && license == "" && !initReadme, - DefaultBranch: "master", + OwnerId: user.Id, + Name: name, + LowerName: strings.ToLower(name), + Description: desc, + IsPrivate: private, + IsBare: lang == "" && license == "" && !initReadme, } + if !repo.IsBare { + repo.DefaultBranch = "master" + } + repoPath := RepoPath(user.Name, repo.Name) sess := orm.NewSession() @@ -310,16 +313,14 @@ func CreateRepository(user *User, name, desc, lang, license string, private, mir return nil, err } - if !repo.IsPrivate { - if err = NewRepoAction(user, repo); err != nil { - log.Error("repo.CreateRepository(NewRepoAction): %v", err) - } - } - if err = WatchRepo(user.Id, repo.Id, true); err != nil { log.Error("repo.CreateRepository(WatchRepo): %v", err) } + if err = NewRepoAction(user, repo); err != nil { + log.Error("repo.CreateRepository(NewRepoAction): %v", err) + } + // No need for init for mirror. if mirror { return repo, nil @@ -714,6 +715,20 @@ func GetRepositoryCount(user *User) (int64, error) { return orm.Count(&Repository{OwnerId: user.Id}) } +// GetCollaborators returns a list of user name of repository's collaborators. +func GetCollaborators(repoName string) ([]string, error) { + accesses := make([]*Access, 0, 10) + if err := orm.Find(&accesses, &Access{RepoName: strings.ToLower(repoName)}); err != nil { + return nil, err + } + + names := make([]string, len(accesses)) + for i := range accesses { + names[i] = accesses[i].UserName + } + return names, nil +} + // Watch is connection request for receiving repository notifycation. type Watch struct { Id int64 diff --git a/models/user.go b/models/user.go index 6751d11d..e340eed4 100644 --- a/models/user.go +++ b/models/user.go @@ -46,6 +46,7 @@ type User struct { Id int64 LowerName string `xorm:"unique not null"` Name string `xorm:"unique not null"` + FullName string Email string `xorm:"unique not null"` Passwd string `xorm:"not null"` LoginType int @@ -229,9 +230,9 @@ func ChangeUserName(user *User, newUserName string) (err error) { accesses[i].UserName = newUserName if strings.HasPrefix(accesses[i].RepoName, user.LowerName+"/") { accesses[i].RepoName = strings.Replace(accesses[i].RepoName, user.LowerName, newUserName, 1) - if err = UpdateAccessWithSession(sess, &accesses[i]); err != nil { - return err - } + } + if err = UpdateAccessWithSession(sess, &accesses[i]); err != nil { + return err } } @@ -247,6 +248,7 @@ func ChangeUserName(user *User, newUserName string) (err error) { } for j := range accesses { + accesses[j].UserName = newUserName accesses[j].RepoName = newUserName + "/" + repos[i].LowerName if err = UpdateAccessWithSession(sess, &accesses[j]); err != nil { return err diff --git a/modules/auth/auth.go b/modules/auth/auth.go index e493faef..96e3868f 100644 --- a/modules/auth/auth.go +++ b/modules/auth/auth.go @@ -21,7 +21,7 @@ type Form interface { } type RegisterForm struct { - UserName string `form:"username" binding:"Required;AlphaDash;MaxSize(30)"` + UserName string `form:"username" binding:"Required;AlphaDashDot;MaxSize(30)"` Email string `form:"email" binding:"Required;Email;MaxSize(50)"` Password string `form:"passwd" binding:"Required;MinSize(6);MaxSize(30)"` RetypePasswd string `form:"retypepasswd"` @@ -123,6 +123,8 @@ func validate(errors *base.BindingErrors, data base.TmplData, form Form) { data["ErrorMsg"] = form.Name(field.Name) + " cannot be empty" case base.BindingAlphaDashError: data["ErrorMsg"] = form.Name(field.Name) + " must be valid alpha or numeric or dash(-_) characters" + case base.BindingAlphaDashDotError: + data["ErrorMsg"] = form.Name(field.Name) + " must be valid alpha or numeric or dash(-_) or dot characters" case base.BindingMinSizeError: data["ErrorMsg"] = form.Name(field.Name) + " must contain at least " + getMinMaxSize(field) + " characters" case base.BindingMaxSizeError: @@ -174,7 +176,7 @@ type InstallForm struct { RunUser string `form:"run_user"` Domain string `form:"domain"` AppUrl string `form:"app_url"` - AdminName string `form:"admin_name" binding:"Required"` + AdminName string `form:"admin_name" binding:"Required;AlphaDashDot;MaxSize(30)"` AdminPasswd string `form:"admin_pwd" binding:"Required;MinSize(6);MaxSize(30)"` AdminEmail string `form:"admin_email" binding:"Required;Email;MaxSize(50)"` SmtpHost string `form:"smtp_host"` diff --git a/modules/auth/user.go b/modules/auth/user.go index 97389422..8d60670d 100644 --- a/modules/auth/user.go +++ b/modules/auth/user.go @@ -75,6 +75,7 @@ type FeedsForm struct { type UpdateProfileForm struct { UserName string `form:"username" binding:"Required;AlphaDash;MaxSize(30)"` + FullName string `form:"fullname" binding:"MaxSize(40)"` Email string `form:"email" binding:"Required;Email;MaxSize(50)"` Website string `form:"website" binding:"MaxSize(50)"` Location string `form:"location" binding:"MaxSize(50)"` diff --git a/modules/base/base.go b/modules/base/base.go index 3e80a436..e6befb83 100644 --- a/modules/base/base.go +++ b/modules/base/base.go @@ -45,6 +45,7 @@ func (this *BindingErrors) Combine(other BindingErrors) { const ( BindingRequireError string = "Required" BindingAlphaDashError string = "AlphaDash" + BindingAlphaDashDotError string = "AlphaDashDot" BindingMinSizeError string = "MinSize" BindingMaxSizeError string = "MaxSize" BindingEmailError string = "Email" diff --git a/modules/middleware/binding.go b/modules/middleware/binding.go index cde9ae9c..bff34ddd 100644 --- a/modules/middleware/binding.go +++ b/modules/middleware/binding.go @@ -180,9 +180,10 @@ func Validate(obj interface{}) martini.Handler { } var ( - alphaDashPattern = regexp.MustCompile("[^\\d\\w-_]") - emailPattern = regexp.MustCompile("[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?") - urlPattern = regexp.MustCompile(`(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?`) + alphaDashPattern = regexp.MustCompile("[^\\d\\w-_]") + alphaDashDotPattern = regexp.MustCompile("[^\\d\\w-_\\.]") + emailPattern = regexp.MustCompile("[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?") + urlPattern = regexp.MustCompile(`(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?`) ) func validateStruct(errors *base.BindingErrors, obj interface{}) { @@ -227,6 +228,11 @@ func validateStruct(errors *base.BindingErrors, obj interface{}) { errors.Fields[field.Name] = base.BindingAlphaDashError break } + case rule == "AlphaDashDot": + if alphaDashDotPattern.MatchString(fmt.Sprintf("%v", fieldValue)) { + errors.Fields[field.Name] = base.BindingAlphaDashDotError + break + } case strings.HasPrefix(rule, "MinSize("): min, err := strconv.Atoi(rule[8 : len(rule)-1]) if err != nil { diff --git a/modules/middleware/repo.go b/modules/middleware/repo.go index e31deac5..c31090b4 100644 --- a/modules/middleware/repo.go +++ b/modules/middleware/repo.go @@ -194,9 +194,17 @@ func RepoAssignment(redirect bool, args ...bool) martini.Handler { } } else { - refName = ctx.Repo.Repository.DefaultBranch if len(refName) == 0 { - refName = "master" + if gitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) { + refName = ctx.Repo.Repository.DefaultBranch + } else { + brs, err := gitRepo.GetBranches() + if err != nil { + ctx.Handle(500, "RepoAssignment(GetBranches))", err) + return + } + refName = brs[0] + } } goto detect } diff --git a/public/css/gogs.css b/public/css/gogs.css index 12b6d8b0..e84eb1ef 100755 --- a/public/css/gogs.css +++ b/public/css/gogs.css @@ -311,11 +311,18 @@ html, body { border-radius: 2px; } -#user-name { - margin-top: 20px; +#user-name,#user-full-name { font-size: 1.6em; font-weight: bold; +} + +#user-name{ margin-bottom: 20px; + margin-top: 10px; +} + +#user-full-name{ + margin-top: 20px; } #user-profile .profile-info .list-group-item { @@ -387,6 +394,13 @@ html, body { /* gogits user setting */ +#user-setting-nav.repo-setting-nav { + background-color: #FFF; + border: 1px solid #CCC; + padding: 0; + padding-top: 10px; +} + #user-setting-nav > h4, #user-setting-container > h4, #user-setting-container > div > h4, #ssh-keys > h4, #user-delete > h4, #repo-setting-container .tab-pane > h4 { padding-bottom: 18px; @@ -396,13 +410,14 @@ html, body { #user-setting-nav .list-group .list-group-item a { margin-left: 0; - padding: .6em; + padding: .6em 1.2em; font-size: 14px; color: #3B73AF; } #user-setting-nav .list-group .list-group-item { background-color: transparent; + margin-bottom: .6em; } #user-setting-nav .list-group .list-group-item-success a { @@ -431,10 +446,60 @@ html, body { border-left: 4px solid #DD4B39; } +#repo-setting-container{ + padding-right: 0; +} + #repo-setting-container .form-horizontal label { line-height: 30px; } +#repo-collab-list li.collab{ + margin-bottom: .6em; +} + +#repo-collab-list .avatar{ + margin-right: 1em; + width: 40px; +} + +#repo-collab-list a.member{ + color: #444; +} + +#repo-collab-list .remove-collab{ + color: #DD4B39; +} + +#repo-collab-form .dropdown-menu{ + margin-left: 15px; + margin-top: 4px; + padding: 0; +} + +#repo-collab-form .dropdown-menu li{ + padding: 0 1em; + line-height: 36px; + cursor: pointer; + font-weight: bold; +} + +#repo-collab-form .dropdown-menu li:hover{ + background-color: #e8f0ff; +} + +#repo-collab-form .dropdown-menu img{ + width: 28px; + height: 28px; + margin-right: 1em; + vertical-align: middle; + margin-top: -3px; +} + +#repo-collab-form .dropdown-menu ul{ + margin-bottom: 0; +} + /* gogits user ssh keys */ #ssh-keys .list-group-item { @@ -649,6 +714,10 @@ html, body { padding: 0; } +#repo-toolbar ul.navbar-right { + margin-right: 0; +} + .activity-list { font-size: 14px; } diff --git a/public/js/app.js b/public/js/app.js index 30e9d5d0..59fffd36 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -240,7 +240,7 @@ var Gogits = { } }); - $(window).on('hashchange',function (e) { + $(window).on('hashchange', function (e) { var m = window.location.hash.match(/^#(L\d+)\-(L\d+)$/); var $list = $('.code-view ol.linenums > li'); if (m) { @@ -387,7 +387,7 @@ function initRepository() { var $clone = $('.clone-group-btn'); if ($clone.length) { var $url = $('.clone-group-url'); - $clone.find('button[data-link]').on("click",function (e) { + $clone.find('button[data-link]').on("click", function (e) { var $this = $(this); if (!$this.hasClass('btn-primary')) { $clone.find('.input-group-btn .btn-primary').removeClass('btn-primary').addClass("btn-default"); @@ -408,7 +408,7 @@ function initRepository() { var $watch = $('#repo-watching'), watchLink = $watch.data("watch"), unwatchLink = $watch.data("unwatch"); - $watch.on('click', '.to-watch',function () { + $watch.on('click', '.to-watch', function () { if ($watch.hasClass("watching")) { return false; } @@ -468,7 +468,7 @@ function initRepository() { function initInstall() { // database type change (function () { - var mysql_default = '127.0.0.1:3306' + var mysql_default = '127.0.0.1:3306' var postgres_default = '127.0.0.1:5432' $('#install-database').on("change", function () { @@ -585,6 +585,39 @@ function initRelease() { }()); } +function initRepoSetting() { + // repo member add + $('#repo-collaborator').on('keyup', function () { + var $this = $(this); + if (!$this.val()) { + $this.next().toggleHide(); + return; + } + $.ajax({ + url: '/api/v1/users/search?q=' + $this.val(), + dataType: "json", + success: function (json) { + if (json.ok && json.data.length) { + var html = ''; + $.each(json.data, function (i, item) { + html += '<li><img src="' + item.avatar + '">' + item.username + '</li>'; + }); + $this.next().toggleShow(); + $this.next().find('ul').html(html); + }else{ + $this.next().toggleHide(); + } + } + }); + }).on('focus', function () { + if (!$(this).val()) { + $(this).next().toggleHide(); + } + }).next().on("click",'li',function(){ + $('#repo-collaborator').val($(this).text()); + }); +} + (function ($) { $(function () { initCore(); @@ -607,5 +640,8 @@ function initRelease() { if ($('#release').length) { initRelease(); } + if ($('#repo-setting-container').length) { + initRepoSetting(); + } }); })(jQuery); diff --git a/routers/repo/repo.go b/routers/repo/repo.go index 76964dff..e82c6ae9 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -286,119 +286,6 @@ func authRequired(ctx *middleware.Context) { ctx.HTML(401, fmt.Sprintf("status/401")) } -func Setting(ctx *middleware.Context, params martini.Params) { - if !ctx.Repo.IsOwner { - ctx.Handle(404, "repo.Setting", nil) - return - } - - ctx.Data["IsRepoToolbarSetting"] = true - - var title string - if t, ok := ctx.Data["Title"].(string); ok { - title = t - } - - ctx.Data["Title"] = title + " - settings" - ctx.HTML(200, "repo/setting") -} - -func SettingPost(ctx *middleware.Context) { - if !ctx.Repo.IsOwner { - ctx.Error(404) - return - } - - ctx.Data["IsRepoToolbarSetting"] = true - - switch ctx.Query("action") { - case "update": - newRepoName := ctx.Query("name") - // Check if repository name has been changed. - if ctx.Repo.Repository.Name != newRepoName { - isExist, err := models.IsRepositoryExist(ctx.Repo.Owner, newRepoName) - if err != nil { - ctx.Handle(500, "repo.SettingPost(update: check existence)", err) - return - } else if isExist { - ctx.RenderWithErr("Repository name has been taken in your repositories.", "repo/setting", nil) - return - } else if err = models.ChangeRepositoryName(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name, newRepoName); err != nil { - ctx.Handle(500, "repo.SettingPost(change repository name)", err) - return - } - log.Trace("%s Repository name changed: %s/%s -> %s", ctx.Req.RequestURI, ctx.User.Name, ctx.Repo.Repository.Name, newRepoName) - - ctx.Repo.Repository.Name = newRepoName - } - - br := ctx.Query("branch") - - if git.IsBranchExist(models.RepoPath(ctx.User.Name, ctx.Repo.Repository.Name), br) { - ctx.Repo.Repository.DefaultBranch = br - } - ctx.Repo.Repository.Description = ctx.Query("desc") - ctx.Repo.Repository.Website = ctx.Query("site") - ctx.Repo.Repository.IsPrivate = ctx.Query("private") == "on" - ctx.Repo.Repository.IsGoget = ctx.Query("goget") == "on" - if err := models.UpdateRepository(ctx.Repo.Repository); err != nil { - ctx.Handle(404, "repo.SettingPost(update)", err) - return - } - log.Trace("%s Repository updated: %s/%s", ctx.Req.RequestURI, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) - - if ctx.Repo.Repository.IsMirror { - if len(ctx.Query("interval")) > 0 { - var err error - ctx.Repo.Mirror.Interval, err = base.StrTo(ctx.Query("interval")).Int() - if err != nil { - log.Error("repo.SettingPost(get mirror interval): %v", err) - } else if err = models.UpdateMirror(ctx.Repo.Mirror); err != nil { - log.Error("repo.SettingPost(UpdateMirror): %v", err) - } - } - } - - ctx.Flash.Success("Repository options has been successfully updated.") - ctx.Redirect(fmt.Sprintf("/%s/%s/settings", ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)) - case "transfer": - if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { - ctx.RenderWithErr("Please make sure you entered repository name is correct.", "repo/setting", nil) - return - } - - newOwner := ctx.Query("owner") - // Check if new owner exists. - isExist, err := models.IsUserExist(newOwner) - if err != nil { - ctx.Handle(500, "repo.SettingPost(transfer: check existence)", err) - return - } else if !isExist { - ctx.RenderWithErr("Please make sure you entered owner name is correct.", "repo/setting", nil) - return - } else if err = models.TransferOwnership(ctx.User, newOwner, ctx.Repo.Repository); err != nil { - ctx.Handle(500, "repo.SettingPost(transfer repository)", err) - return - } - log.Trace("%s Repository transfered: %s/%s -> %s", ctx.Req.RequestURI, ctx.User.Name, ctx.Repo.Repository.Name, newOwner) - - ctx.Redirect("/") - case "delete": - if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { - ctx.RenderWithErr("Please make sure you entered repository name is correct.", "repo/setting", nil) - return - } - - if err := models.DeleteRepository(ctx.User.Id, ctx.Repo.Repository.Id, ctx.User.LowerName); err != nil { - ctx.Handle(500, "repo.Delete", err) - return - } - log.Trace("%s Repository deleted: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.LowerName) - - ctx.Redirect("/") - } -} - func Action(ctx *middleware.Context, params martini.Params) { var err error switch params["action"] { diff --git a/routers/repo/setting.go b/routers/repo/setting.go new file mode 100644 index 00000000..8f7b84b6 --- /dev/null +++ b/routers/repo/setting.go @@ -0,0 +1,206 @@ +// 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" + + "github.com/gogits/git" + + "github.com/gogits/gogs/models" + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/middleware" +) + +func Setting(ctx *middleware.Context) { + if !ctx.Repo.IsOwner { + ctx.Handle(404, "repo.Setting", nil) + return + } + + ctx.Data["IsRepoToolbarSetting"] = true + ctx.Data["Title"] = strings.TrimPrefix(ctx.Repo.RepoLink, "/") + " - settings" + ctx.HTML(200, "repo/setting") +} + +func SettingPost(ctx *middleware.Context) { + if !ctx.Repo.IsOwner { + ctx.Error(404) + return + } + + ctx.Data["IsRepoToolbarSetting"] = true + + switch ctx.Query("action") { + case "update": + newRepoName := ctx.Query("name") + // Check if repository name has been changed. + if ctx.Repo.Repository.Name != newRepoName { + isExist, err := models.IsRepositoryExist(ctx.Repo.Owner, newRepoName) + if err != nil { + ctx.Handle(500, "repo.SettingPost(update: check existence)", err) + return + } else if isExist { + ctx.RenderWithErr("Repository name has been taken in your repositories.", "repo/setting", nil) + return + } else if err = models.ChangeRepositoryName(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name, newRepoName); err != nil { + ctx.Handle(500, "repo.SettingPost(change repository name)", err) + return + } + log.Trace("%s Repository name changed: %s/%s -> %s", ctx.Req.RequestURI, ctx.User.Name, ctx.Repo.Repository.Name, newRepoName) + + ctx.Repo.Repository.Name = newRepoName + } + + br := ctx.Query("branch") + + if git.IsBranchExist(models.RepoPath(ctx.User.Name, ctx.Repo.Repository.Name), br) { + ctx.Repo.Repository.DefaultBranch = br + } + ctx.Repo.Repository.Description = ctx.Query("desc") + ctx.Repo.Repository.Website = ctx.Query("site") + ctx.Repo.Repository.IsPrivate = ctx.Query("private") == "on" + ctx.Repo.Repository.IsGoget = ctx.Query("goget") == "on" + if err := models.UpdateRepository(ctx.Repo.Repository); err != nil { + ctx.Handle(404, "repo.SettingPost(update)", err) + return + } + log.Trace("%s Repository updated: %s/%s", ctx.Req.RequestURI, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) + + if ctx.Repo.Repository.IsMirror { + if len(ctx.Query("interval")) > 0 { + var err error + ctx.Repo.Mirror.Interval, err = base.StrTo(ctx.Query("interval")).Int() + if err != nil { + log.Error("repo.SettingPost(get mirror interval): %v", err) + } else if err = models.UpdateMirror(ctx.Repo.Mirror); err != nil { + log.Error("repo.SettingPost(UpdateMirror): %v", err) + } + } + } + + ctx.Flash.Success("Repository options has been successfully updated.") + ctx.Redirect(fmt.Sprintf("/%s/%s/settings", ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)) + case "transfer": + if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { + ctx.RenderWithErr("Please make sure you entered repository name is correct.", "repo/setting", nil) + return + } + + newOwner := ctx.Query("owner") + // Check if new owner exists. + isExist, err := models.IsUserExist(newOwner) + if err != nil { + ctx.Handle(500, "repo.SettingPost(transfer: check existence)", err) + return + } else if !isExist { + ctx.RenderWithErr("Please make sure you entered owner name is correct.", "repo/setting", nil) + return + } else if err = models.TransferOwnership(ctx.User, newOwner, ctx.Repo.Repository); err != nil { + ctx.Handle(500, "repo.SettingPost(transfer repository)", err) + return + } + log.Trace("%s Repository transfered: %s/%s -> %s", ctx.Req.RequestURI, ctx.User.Name, ctx.Repo.Repository.Name, newOwner) + + ctx.Redirect("/") + case "delete": + if len(ctx.Repo.Repository.Name) == 0 || ctx.Repo.Repository.Name != ctx.Query("repository") { + ctx.RenderWithErr("Please make sure you entered repository name is correct.", "repo/setting", nil) + return + } + + if err := models.DeleteRepository(ctx.User.Id, ctx.Repo.Repository.Id, ctx.User.LowerName); err != nil { + ctx.Handle(500, "repo.Delete", err) + return + } + log.Trace("%s Repository deleted: %s/%s", ctx.Req.RequestURI, ctx.User.LowerName, ctx.Repo.Repository.LowerName) + + ctx.Redirect("/") + } +} + +func Collaboration(ctx *middleware.Context) { + if !ctx.Repo.IsOwner { + ctx.Error(404) + return + } + + repoLink := strings.TrimPrefix(ctx.Repo.RepoLink, "/") + ctx.Data["IsRepoToolbarCollaboration"] = true + ctx.Data["Title"] = repoLink + " - collaboration" + + // Delete collaborator. + remove := strings.ToLower(ctx.Query("remove")) + if len(remove) > 0 && remove != ctx.Repo.Owner.LowerName { + if err := models.DeleteAccess(&models.Access{UserName: remove, RepoName: repoLink}); err != nil { + ctx.Handle(500, "repo.Collaboration(DeleteAccess)", err) + return + } + ctx.Flash.Success("Collaborator has been removed.") + ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") + return + } + + names, err := models.GetCollaborators(repoLink) + if err != nil { + ctx.Handle(500, "repo.Collaboration(GetCollaborators)", err) + return + } + + us := make([]*models.User, len(names)) + for i, name := range names { + us[i], err = models.GetUserByName(name) + if err != nil { + ctx.Handle(500, "repo.Collaboration(GetUserByName)", err) + return + } + } + + ctx.Data["Collaborators"] = us + ctx.HTML(200, "repo/collaboration") +} + +func CollaborationPost(ctx *middleware.Context) { + if !ctx.Repo.IsOwner { + ctx.Error(404) + return + } + + repoLink := strings.TrimPrefix(ctx.Repo.RepoLink, "/") + name := strings.ToLower(ctx.Query("collaborator")) + if len(name) == 0 || ctx.Repo.Owner.LowerName == name { + ctx.Redirect(ctx.Req.RequestURI) + return + } + has, err := models.HasAccess(name, repoLink, models.AU_WRITABLE) + if err != nil { + ctx.Handle(500, "repo.CollaborationPost(HasAccess)", err) + return + } else if has { + ctx.Redirect(ctx.Req.RequestURI) + return + } + + isExist, err := models.IsUserExist(name) + if err != nil { + ctx.Handle(500, "repo.CollaborationPost(IsUserExist)", err) + return + } else if !isExist { + ctx.Flash.Error("Given user does not exist.") + ctx.Redirect(ctx.Req.RequestURI) + return + } + + if err := models.AddAccess(&models.Access{UserName: name, RepoName: repoLink, + Mode: models.AU_WRITABLE}); err != nil { + ctx.Handle(500, "repo.CollaborationPost(AddAccess)", err) + return + } + + ctx.Flash.Success("New collaborator has been added.") + ctx.Redirect(ctx.Req.RequestURI) +} diff --git a/routers/user/setting.go b/routers/user/setting.go index a55e617f..f62e9310 100644 --- a/routers/user/setting.go +++ b/routers/user/setting.go @@ -54,6 +54,7 @@ func SettingPost(ctx *middleware.Context, form auth.UpdateProfileForm) { user.Name = form.UserName } + user.FullName = form.FullName user.Email = form.Email user.Website = form.Website user.Location = form.Location diff --git a/templates/base/navbar.tmpl b/templates/base/navbar.tmpl index da6865b8..80085d97 100644 --- a/templates/base/navbar.tmpl +++ b/templates/base/navbar.tmpl @@ -3,7 +3,7 @@ <nav class="nav"> <a id="nav-logo" class="nav-item pull-left{{if .PageIsHome}} active{{end}}" href="/"><img src="/img/favicon.png" alt="Gogs Logo" id="logo"></a> <a class="nav-item pull-left{{if .PageIsUserDashboard}} active{{end}}" href="/">Dashboard</a> - <a class="nav-item pull-left{{if .PageIsHelp}} active{{end}}" target="_blank" href="https://github.com/gogits/gogs/wiki">Help</a>{{if .IsSigned}} + <a class="nav-item pull-left{{if .PageIsHelp}} active{{end}}" target="_blank" href="http://gogs.io/docs">Help</a>{{if .IsSigned}} {{if .HasAccess}}<form class="nav-item pull-left{{if .PageIsNewRepo}} active{{end}}" id="nav-search-form"> <div class="input-group"> <div class="input-group-btn"> diff --git a/templates/repo/collaboration.tmpl b/templates/repo/collaboration.tmpl new file mode 100644 index 00000000..8ede9d43 --- /dev/null +++ b/templates/repo/collaboration.tmpl @@ -0,0 +1,45 @@ +{{template "base/head" .}} +{{template "base/navbar" .}} +{{template "repo/nav" .}} +{{template "repo/toolbar" .}} +<div id="body" class="container"> + {{template "repo/setting_nav" .}} + <div id="repo-setting-container" class="col-md-10"> + {{template "base/alert" .}} + <div class="panel panel-default"> + <div class="panel-heading"> + Collaborators + </div> + <div class="panel-body"> + <ul id="repo-collab-list" class="list-unstyled"> + {{range .Collaborators}} + <li class="collab"> + {{if not (eq .LowerName $.Owner.LowerName)}}<a href="{{$.RepoLink}}/settings/collaboration?remove={{.Name}}" class="remove-collab pull-right"><i class="fa fa-times"></i></a>{{end}} + <a class="member" href="/user/{{.Name}}"> + <img alt="{{.Name}}" class="pull-left avatar" src="{{.AvatarLink}}"> + <strong class="access-member-fullname">{{.FullName}}</strong><br/> + {{.Name}} + </a> + </li> + {{end}} + </ul> + </div> + <div class="panel-footer"> + <form action="{{.RepoLink}}/settings/collaboration" method="post" class="form-horizontal" id="repo-collab-form"> + {{.CsrfTokenHtml}} + <div class="form-group" style="margin-bottom: 0"> + <div class="col-md-4"> + <input type="text" name="collaborator" class="form-control dropdown-toggle" id="repo-collaborator" autocomplete="off" required="required" data-toggle="dropdown"/> + <div class="dropdown-menu"> + <ul class="list-unstyled"></ul> + </div> + </div> + <button class="col-md-2 btn btn-primary">Add collaborator</button> + </div> + </form> + </div> + </div> + + </div> +</div> +{{template "base/footer" .}}
\ No newline at end of file diff --git a/templates/repo/setting.tmpl b/templates/repo/setting.tmpl index 61621fe0..f0f041de 100644 --- a/templates/repo/setting.tmpl +++ b/templates/repo/setting.tmpl @@ -3,15 +3,8 @@ {{template "repo/nav" .}} {{template "repo/toolbar" .}} <div id="body" class="container"> - <div id="user-setting-nav" class="col-md-3"> - <ul class="list-group"> - <li class="list-group-item active"><a href="/{{.Owner.Name}}/{{.Repository.Name}}/settings">Options</a></li> - <!--<li class="list-group-item"><a href="#">Collaborators</a></li> - <li class="list-group-item"><a href="#">Notifications</a></li>--> - </ul> - </div> - - <div id="repo-setting-container" class="col-md-9"> + {{template "repo/setting_nav" .}} + <div id="repo-setting-container" class="col-md-10"> {{template "base/alert" .}} <div class="panel panel-default"> <div class="panel-heading"> @@ -48,7 +41,7 @@ <label class="col-md-3 text-right">Default Branch</label> <div class="col-md-3"> <select name="branch" id="repo-default-branch" class="form-control"> - <option value="{{.Repository.DefaultBranch}}">{{.Repository.DefaultBranch}}</option> + {{if .Repository.DefaultBranch}}<option value="{{.Repository.DefaultBranch}}">{{.Repository.DefaultBranch}}</option>{{end}} {{range .Branches}} {{if eq . $.Repository.DefaultBranch}}{{else}}<option value="{{.}}">{{.}}</option>{{end}} {{end}} diff --git a/templates/repo/setting_nav.tmpl b/templates/repo/setting_nav.tmpl new file mode 100644 index 00000000..4e558a76 --- /dev/null +++ b/templates/repo/setting_nav.tmpl @@ -0,0 +1,7 @@ +<div id="user-setting-nav" class="col-md-2 repo-setting-nav"> + <ul class="list-group"> + <li class="list-group-item{{if .IsRepoToolbarSetting}} active{{end}}"><a href="/{{.Owner.Name}}/{{.Repository.Name}}/settings">Options</a></li> + <li class="list-group-item{{if .IsRepoToolbarCollaboration}} active{{end}}"><a href="/{{.Owner.Name}}/{{.Repository.Name}}/settings/collaboration">Collaborators</a></li> + <!--<li class="list-group-item"><a href="#">Notifications</a></li>--> + </ul> +</div>
\ No newline at end of file diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index 15d2a0bd..4c42be24 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -6,6 +6,7 @@ <a href="http://gravatar.com/emails/" class="center-block" data-toggle="tooltip" data-placement="bottom" title="Change your avatar at gravatar.com"> <img id="user-avatar" src="{{.Owner.AvatarLink}}?s=200" alt="user-avatar" title="{{.Owner.Name}}"/> </a> + {{if .Owner.FullName}}<span id="user-full-name" class="center-block">{{.Owner.FullName}}</span>{{end}} <span id="user-name" class="center-block">{{.Owner.Name}}</span> </div> <div class="profile-info"> diff --git a/templates/user/setting.tmpl b/templates/user/setting.tmpl index 56d859fe..bdcf9ce3 100644 --- a/templates/user/setting.tmpl +++ b/templates/user/setting.tmpl @@ -18,6 +18,13 @@ </div> <div class="form-group"> + <label class="col-md-2 control-label">Full Name</label> + <div class="col-md-8"> + <input name="fullname" class="form-control" placeholder="Type your full name" value="{{.SignedUser.FullName}}"> + </div> + </div> + + <div class="form-group"> <label class="col-md-2 control-label">Email<strong class="text-danger">*</strong></label> <div class="col-md-8"> <input name="email" class="form-control" placeholder="Type your e-mail address" required="required" value="{{.SignedUser.Email}}"> @@ -121,10 +121,10 @@ func runWeb(*cli.Context) { m.Get("/user/:username", ignSignIn, user.Profile) m.Group("/repo", func(r martini.Router) { - m.Get("/create", repo.Create) - m.Post("/create", bindIgnErr(auth.CreateRepoForm{}), repo.CreatePost) - m.Get("/migrate", repo.Migrate) - m.Post("/migrate", bindIgnErr(auth.MigrateRepoForm{}), repo.MigratePost) + r.Get("/create", repo.Create) + r.Post("/create", bindIgnErr(auth.CreateRepoForm{}), repo.CreatePost) + r.Get("/migrate", repo.Migrate) + r.Post("/migrate", bindIgnErr(auth.MigrateRepoForm{}), repo.MigratePost) }, reqSignIn) adminReq := middleware.Toggle(&middleware.ToggleOptions{SignInRequire: true, AdminRequire: true}) @@ -150,6 +150,8 @@ func runWeb(*cli.Context) { m.Group("/:username/:reponame", func(r martini.Router) { r.Get("/settings", repo.Setting) r.Post("/settings", repo.SettingPost) + r.Get("/settings/collaboration", repo.Collaboration) + r.Post("/settings/collaboration", repo.CollaborationPost) r.Get("/action/:action", repo.Action) r.Get("/issues/new", repo.CreateIssue) r.Post("/issues/new", bindIgnErr(auth.CreateIssueForm{}), repo.CreateIssuePost) |