#1511 Allow local import only for admin users

This commit is contained in:
Unknwon 2015-11-03 18:40:52 -05:00
parent 25ec20d525
commit 6f0a41b8b2
18 changed files with 152 additions and 79 deletions

View file

@ -5,7 +5,7 @@ Gogs - Go Git Service [![Build Status](https://travis-ci.org/gogits/gogs.svg?bra
![](public/img/gogs-large-resize.png) ![](public/img/gogs-large-resize.png)
##### Current version: 0.6.23 Beta ##### Current version: 0.6.24 Beta
<table> <table>
<tr> <tr>

View file

@ -360,6 +360,7 @@ migrate_type_helper=Diese Repository wird ein <span class="text blue">Spiegel</s
migrate_repo=Repository migrieren migrate_repo=Repository migrieren
migrate.clone_address=Adresse kopieren migrate.clone_address=Adresse kopieren
migrate.clone_address_desc=Dies kann eine HTTP/HTTPS/GIT URL oder ein lokaler Serverpfad sein. migrate.clone_address_desc=Dies kann eine HTTP/HTTPS/GIT URL oder ein lokaler Serverpfad sein.
migrate.permission_denied=You are not allowed to import local repositories.
migrate.invalid_local_path=Lokaler Pfad ist ungültig, er existiert nicht oder ist kein Ordner. migrate.invalid_local_path=Lokaler Pfad ist ungültig, er existiert nicht oder ist kein Ordner.
forked_from=Geforkt von forked_from=Geforkt von
@ -651,7 +652,7 @@ release.tag_name_already_exist=Ein Release mit diesem Tag existiert bereits.
[org] [org]
org_name_holder=Name der Organisation org_name_holder=Name der Organisation
org_full_name_holder=Organization Full Name org_full_name_holder=Vollständiger Name der Organisation
org_name_helper=Gute Namen von Organisationen sind kurz und einprägsam. org_name_helper=Gute Namen von Organisationen sind kurz und einprägsam.
create_org=Organisation erstellen create_org=Organisation erstellen
repo_updated=Aktualisiert repo_updated=Aktualisiert
@ -808,6 +809,7 @@ users.edit_account=Konto bearbeiten
users.is_activated=Dieses Konto ist aktiviert users.is_activated=Dieses Konto ist aktiviert
users.is_admin=Dieses Konto hat Administratorrechte users.is_admin=Dieses Konto hat Administratorrechte
users.allow_git_hook=Dieses Konto ist berechtigt, Git-Hooks zu erstellen users.allow_git_hook=Dieses Konto ist berechtigt, Git-Hooks zu erstellen
users.allow_import_local=This account has permissions to import local repositories
users.update_profile=Kontoprofil aktualisieren users.update_profile=Kontoprofil aktualisieren
users.delete_account=Dieses Konto löschen users.delete_account=Dieses Konto löschen
users.still_own_repo=Dieses Konto besitzt noch Repositories. Diese müssen zuerst gelöscht oder übertragen werden. users.still_own_repo=Dieses Konto besitzt noch Repositories. Diese müssen zuerst gelöscht oder übertragen werden.

View file

@ -360,6 +360,7 @@ migrate_type_helper = This repository will be a <span class="text blue">mirror</
migrate_repo = Migrate Repository migrate_repo = Migrate Repository
migrate.clone_address = Clone Address migrate.clone_address = Clone Address
migrate.clone_address_desc = This can be a HTTP/HTTPS/GIT URL or local server path. migrate.clone_address_desc = This can be a HTTP/HTTPS/GIT URL or local server path.
migrate.permission_denied = You are not allowed to import local repositories.
migrate.invalid_local_path = Invalid local path, it does not exist or not a directory. migrate.invalid_local_path = Invalid local path, it does not exist or not a directory.
forked_from = forked from forked_from = forked from
@ -808,6 +809,7 @@ users.edit_account = Edit Account
users.is_activated = This account is activated users.is_activated = This account is activated
users.is_admin = This account has administrator permissions users.is_admin = This account has administrator permissions
users.allow_git_hook = This account has permissions to create Git hooks users.allow_git_hook = This account has permissions to create Git hooks
users.allow_import_local = This account has permissions to import local repositories
users.update_profile = Update Account Profile users.update_profile = Update Account Profile
users.delete_account = Delete This Account users.delete_account = Delete This Account
users.still_own_repo = This account still has ownership over at least one repository, you have to delete or transfer them first. users.still_own_repo = This account still has ownership over at least one repository, you have to delete or transfer them first.

View file

@ -360,6 +360,7 @@ migrate_type_helper=该仓库将是一个 <span class="text blue">镜像</span>
migrate_repo=迁移仓库 migrate_repo=迁移仓库
migrate.clone_address=克隆地址 migrate.clone_address=克隆地址
migrate.clone_address_desc=该地址可以是 HTTP/HTTPS/GIT URL 或本地服务器路径。 migrate.clone_address_desc=该地址可以是 HTTP/HTTPS/GIT URL 或本地服务器路径。
migrate.permission_denied=您没有获得导入本地仓库的权限。
migrate.invalid_local_path=无效的本地路径,不存在或不是一个目录! migrate.invalid_local_path=无效的本地路径,不存在或不是一个目录!
forked_from=派生自 forked_from=派生自
@ -807,7 +808,8 @@ users.update_profile_success=该用户信息更新成功!
users.edit_account=编辑用户信息 users.edit_account=编辑用户信息
users.is_activated=该用户已被激活 users.is_activated=该用户已被激活
users.is_admin=该用户具有管理员权限 users.is_admin=该用户具有管理员权限
users.allow_git_hook=该帐户具有创建 Git 钩子的权限 users.allow_git_hook=该用户具有创建 Git 钩子的权限
users.allow_import_local=该用户具有导入本地仓库的权限
users.update_profile=更新用户信息 users.update_profile=更新用户信息
users.delete_account=删除该用户 users.delete_account=删除该用户
users.still_own_repo=该帐户仍然是某些仓库的拥有者,您必须先转移或删除它们才能执行删除帐户操作! users.still_own_repo=该帐户仍然是某些仓库的拥有者,您必须先转移或删除它们才能执行删除帐户操作!

View file

@ -360,6 +360,7 @@ migrate_type_helper=該倉庫將是一個 <span class="text blue">鏡像</span>
migrate_repo=遷移倉庫 migrate_repo=遷移倉庫
migrate.clone_address=複製地址 migrate.clone_address=複製地址
migrate.clone_address_desc=該地址可以是 HTTP/HTTPS/GIT URL 或本地服務器路徑。 migrate.clone_address_desc=該地址可以是 HTTP/HTTPS/GIT URL 或本地服務器路徑。
migrate.permission_denied=You are not allowed to import local repositories.
migrate.invalid_local_path=無效的本地路徑,該路徑不存在或不是一個目錄! migrate.invalid_local_path=無效的本地路徑,該路徑不存在或不是一個目錄!
forked_from=派生自 forked_from=派生自
@ -808,6 +809,7 @@ users.edit_account=編輯用戶信息
users.is_activated=該用戶已被激活 users.is_activated=該用戶已被激活
users.is_admin=該用戶具有管理員權限 users.is_admin=該用戶具有管理員權限
users.allow_git_hook=該帳戶具有創建 Git 鉤子的權限 users.allow_git_hook=該帳戶具有創建 Git 鉤子的權限
users.allow_import_local=This account has permissions to import local repositories
users.update_profile=更新用戶信息 users.update_profile=更新用戶信息
users.delete_account=刪除該用戶 users.delete_account=刪除該用戶
users.still_own_repo=該帳戶仍然是某些倉庫的擁有者,您必須先轉移或刪除它們才能執行刪除帳戶操作! users.still_own_repo=該帳戶仍然是某些倉庫的擁有者,您必須先轉移或刪除它們才能執行刪除帳戶操作!

View file

@ -17,7 +17,7 @@ import (
"github.com/gogits/gogs/modules/setting" "github.com/gogits/gogs/modules/setting"
) )
const APP_VER = "0.6.23.1103 Beta" const APP_VER = "0.6.24.1103 Beta"
func init() { func init() {
runtime.GOMAXPROCS(runtime.NumCPU()) runtime.GOMAXPROCS(runtime.NumCPU())

View file

@ -18,7 +18,7 @@ func IsErrNameReserved(err error) bool {
} }
func (err ErrNameReserved) Error() string { func (err ErrNameReserved) Error() string {
return fmt.Sprintf("name is reserved: [name: %s]", err.Name) return fmt.Sprintf("name is reserved [name: %s]", err.Name)
} }
type ErrNamePatternNotAllowed struct { type ErrNamePatternNotAllowed struct {
@ -31,7 +31,7 @@ func IsErrNamePatternNotAllowed(err error) bool {
} }
func (err ErrNamePatternNotAllowed) Error() string { func (err ErrNamePatternNotAllowed) Error() string {
return fmt.Sprintf("name pattern is not allowed: [pattern: %s]", err.Pattern) return fmt.Sprintf("name pattern is not allowed [pattern: %s]", err.Pattern)
} }
// ____ ___ // ____ ___
@ -51,7 +51,7 @@ func IsErrUserAlreadyExist(err error) bool {
} }
func (err ErrUserAlreadyExist) Error() string { func (err ErrUserAlreadyExist) Error() string {
return fmt.Sprintf("user already exists: [name: %s]", err.Name) return fmt.Sprintf("user already exists [name: %s]", err.Name)
} }
type ErrUserNotExist struct { type ErrUserNotExist struct {
@ -65,7 +65,7 @@ func IsErrUserNotExist(err error) bool {
} }
func (err ErrUserNotExist) Error() string { func (err ErrUserNotExist) Error() string {
return fmt.Sprintf("user does not exist: [uid: %d, name: %s]", err.UID, err.Name) return fmt.Sprintf("user does not exist [uid: %d, name: %s]", err.UID, err.Name)
} }
type ErrEmailAlreadyUsed struct { type ErrEmailAlreadyUsed struct {
@ -78,7 +78,7 @@ func IsErrEmailAlreadyUsed(err error) bool {
} }
func (err ErrEmailAlreadyUsed) Error() string { func (err ErrEmailAlreadyUsed) Error() string {
return fmt.Sprintf("e-mail has been used: [email: %s]", err.Email) return fmt.Sprintf("e-mail has been used [email: %s]", err.Email)
} }
type ErrUserOwnRepos struct { type ErrUserOwnRepos struct {
@ -91,7 +91,7 @@ func IsErrUserOwnRepos(err error) bool {
} }
func (err ErrUserOwnRepos) Error() string { func (err ErrUserOwnRepos) Error() string {
return fmt.Sprintf("user still has ownership of repositories: [uid: %d]", err.UID) return fmt.Sprintf("user still has ownership of repositories [uid: %d]", err.UID)
} }
type ErrUserHasOrgs struct { type ErrUserHasOrgs struct {
@ -104,7 +104,7 @@ func IsErrUserHasOrgs(err error) bool {
} }
func (err ErrUserHasOrgs) Error() string { func (err ErrUserHasOrgs) Error() string {
return fmt.Sprintf("user still has membership of organizations: [uid: %d]", err.UID) return fmt.Sprintf("user still has membership of organizations [uid: %d]", err.UID)
} }
// __________ ___. .__ .__ ____ __. // __________ ___. .__ .__ ____ __.
@ -124,7 +124,7 @@ func IsErrKeyNotExist(err error) bool {
} }
func (err ErrKeyNotExist) Error() string { func (err ErrKeyNotExist) Error() string {
return fmt.Sprintf("public key does not exist: [id: %d]", err.ID) return fmt.Sprintf("public key does not exist [id: %d]", err.ID)
} }
type ErrKeyAlreadyExist struct { type ErrKeyAlreadyExist struct {
@ -138,7 +138,7 @@ func IsErrKeyAlreadyExist(err error) bool {
} }
func (err ErrKeyAlreadyExist) Error() string { func (err ErrKeyAlreadyExist) Error() string {
return fmt.Sprintf("public key already exists: [owner_id: %d, content: %s]", err.OwnerID, err.Content) return fmt.Sprintf("public key already exists [owner_id: %d, content: %s]", err.OwnerID, err.Content)
} }
type ErrKeyNameAlreadyUsed struct { type ErrKeyNameAlreadyUsed struct {
@ -152,7 +152,7 @@ func IsErrKeyNameAlreadyUsed(err error) bool {
} }
func (err ErrKeyNameAlreadyUsed) Error() string { func (err ErrKeyNameAlreadyUsed) Error() string {
return fmt.Sprintf("public key already exists: [owner_id: %d, name: %s]", err.OwnerID, err.Name) return fmt.Sprintf("public key already exists [owner_id: %d, name: %s]", err.OwnerID, err.Name)
} }
type ErrDeployKeyAlreadyExist struct { type ErrDeployKeyAlreadyExist struct {
@ -166,7 +166,7 @@ func IsErrDeployKeyAlreadyExist(err error) bool {
} }
func (err ErrDeployKeyAlreadyExist) Error() string { func (err ErrDeployKeyAlreadyExist) Error() string {
return fmt.Sprintf("public key already exists: [key_id: %d, repo_id: %d]", err.KeyID, err.RepoID) return fmt.Sprintf("public key already exists [key_id: %d, repo_id: %d]", err.KeyID, err.RepoID)
} }
type ErrDeployKeyNameAlreadyUsed struct { type ErrDeployKeyNameAlreadyUsed struct {
@ -180,7 +180,7 @@ func IsErrDeployKeyNameAlreadyUsed(err error) bool {
} }
func (err ErrDeployKeyNameAlreadyUsed) Error() string { func (err ErrDeployKeyNameAlreadyUsed) Error() string {
return fmt.Sprintf("public key already exists: [repo_id: %d, name: %s]", err.RepoID, err.Name) return fmt.Sprintf("public key already exists [repo_id: %d, name: %s]", err.RepoID, err.Name)
} }
// _____ ___________ __ // _____ ___________ __
@ -200,7 +200,7 @@ func IsErrAccessTokenNotExist(err error) bool {
} }
func (err ErrAccessTokenNotExist) Error() string { func (err ErrAccessTokenNotExist) Error() string {
return fmt.Sprintf("access token does not exist: [sha: %s]", err.SHA) return fmt.Sprintf("access token does not exist [sha: %s]", err.SHA)
} }
// ________ .__ __ .__ // ________ .__ __ .__
@ -220,7 +220,7 @@ func IsErrLastOrgOwner(err error) bool {
} }
func (err ErrLastOrgOwner) Error() string { func (err ErrLastOrgOwner) Error() string {
return fmt.Sprintf("user is the last member of owner team: [uid: %d]", err.UID) return fmt.Sprintf("user is the last member of owner team [uid: %d]", err.UID)
} }
// __________ .__ __ // __________ .__ __
@ -259,6 +259,22 @@ func (err ErrRepoAlreadyExist) Error() string {
return fmt.Sprintf("repository already exists [uname: %s, name: %s]", err.Uname, err.Name) return fmt.Sprintf("repository already exists [uname: %s, name: %s]", err.Uname, err.Name)
} }
type ErrInvalidCloneAddr struct {
IsURLError bool
IsInvalidPath bool
IsPermissionDenied bool
}
func IsErrInvalidCloneAddr(err error) bool {
_, ok := err.(ErrInvalidCloneAddr)
return ok
}
func (err ErrInvalidCloneAddr) Error() string {
return fmt.Sprintf("invalid clone address [is_url_error: %v, is_invalid_path: %v, is_permission_denied: %v]",
err.IsURLError, err.IsInvalidPath, err.IsPermissionDenied)
}
// __ __ ___. .__ __ // __ __ ___. .__ __
// / \ / \ ____\_ |__ | |__ ____ ____ | | __ // / \ / \ ____\_ |__ | |__ ____ ____ | | __
// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ / // \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /

View file

@ -78,6 +78,7 @@ type User struct {
IsActive bool IsActive bool
IsAdmin bool IsAdmin bool
AllowGitHook bool AllowGitHook bool
AllowImportLocal bool // Allow migrate repository by local path
// Avatar. // Avatar.
Avatar string `xorm:"VARCHAR(2048) NOT NULL"` Avatar string `xorm:"VARCHAR(2048) NOT NULL"`
@ -107,6 +108,16 @@ func (u *User) AfterSet(colName string, _ xorm.Cell) {
} }
} }
// CanEditGitHook returns true if user can edit Git hooks.
func (u *User) CanEditGitHook() bool {
return u.IsAdmin || u.AllowGitHook
}
// CanImportLocal returns true if user can migrate repository by local path.
func (u *User) CanImportLocal() bool {
return u.IsAdmin || u.AllowImportLocal
}
// EmailAdresses is the list of all email addresses of a user. Can contain the // EmailAdresses is the list of all email addresses of a user. Can contain the
// primary email address, but is not obligatory // primary email address, but is not obligatory
type EmailAddress struct { type EmailAddress struct {

View file

@ -34,6 +34,7 @@ type AdminEditUserForm struct {
Active bool Active bool
Admin bool Admin bool
AllowGitHook bool AllowGitHook bool
AllowImportLocal bool
} }
func (f *AdminEditUserForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { func (f *AdminEditUserForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {

View file

@ -5,8 +5,14 @@
package auth package auth
import ( import (
"net/url"
"strings"
"github.com/Unknwon/com"
"github.com/go-macaron/binding" "github.com/go-macaron/binding"
"gopkg.in/macaron.v1" "gopkg.in/macaron.v1"
"github.com/gogits/gogs/models"
) )
// _______________________________________ _________.______________________ _______________.___. // _______________________________________ _________.______________________ _______________.___.
@ -46,6 +52,34 @@ func (f *MigrateRepoForm) Validate(ctx *macaron.Context, errs binding.Errors) bi
return validate(errs, ctx.Data, f, ctx.Locale) return validate(errs, ctx.Data, f, ctx.Locale)
} }
// ParseRemoteAddr checks if given remote address is valid,
// and returns composed URL with needed username and passowrd.
// It also checks if given user has permission when remote address
// is actually a local path.
func (f MigrateRepoForm) ParseRemoteAddr(user *models.User) (string, error) {
remoteAddr := f.CloneAddr
// Remote address can be HTTP/HTTPS/Git URL or local path.
if strings.HasPrefix(remoteAddr, "http://") ||
strings.HasPrefix(remoteAddr, "https://") ||
strings.HasPrefix(remoteAddr, "git://") {
u, err := url.Parse(remoteAddr)
if err != nil {
return "", models.ErrInvalidCloneAddr{IsURLError: true}
}
if len(f.AuthUsername)+len(f.AuthPassword) > 0 {
u.User = url.UserPassword(f.AuthUsername, f.AuthPassword)
}
remoteAddr = u.String()
} else if !user.CanImportLocal() {
return "", models.ErrInvalidCloneAddr{IsPermissionDenied: true}
} else if !com.IsDir(remoteAddr) {
return "", models.ErrInvalidCloneAddr{IsInvalidPath: true}
}
return remoteAddr, nil
}
type RepoSettingForm struct { type RepoSettingForm struct {
RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"` RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"`
Description string `binding:"MaxSize(255)"` Description string `binding:"MaxSize(255)"`

File diff suppressed because one or more lines are too long

View file

@ -420,7 +420,7 @@ func RequireRepoAdmin() macaron.Handler {
// GitHookService checks if repository Git hooks service has been enabled. // GitHookService checks if repository Git hooks service has been enabled.
func GitHookService() macaron.Handler { func GitHookService() macaron.Handler {
return func(ctx *Context) { return func(ctx *Context) {
if !ctx.User.AllowGitHook && !ctx.User.IsAdmin { if !ctx.User.CanEditGitHook() {
ctx.Handle(404, "GitHookService", nil) ctx.Handle(404, "GitHookService", nil)
return return
} }

View file

@ -213,6 +213,7 @@ func EditUserPost(ctx *middleware.Context, form auth.AdminEditUserForm) {
u.IsActive = form.Active u.IsActive = form.Active
u.IsAdmin = form.Admin u.IsAdmin = form.Admin
u.AllowGitHook = form.AllowGitHook u.AllowGitHook = form.AllowGitHook
u.AllowImportLocal = form.AllowImportLocal
if err := models.UpdateUser(u); err != nil { if err := models.UpdateUser(u); err != nil {
if models.IsErrEmailAlreadyUsed(err) { if models.IsErrEmailAlreadyUsed(err) {

View file

@ -5,9 +5,7 @@
package v1 package v1
import ( import (
"net/url"
"path" "path"
"strings"
"github.com/Unknwon/com" "github.com/Unknwon/com"
@ -218,22 +216,23 @@ func MigrateRepo(ctx *middleware.Context, form auth.MigrateRepoForm) {
} }
} }
// Remote address can be HTTP/HTTPS/Git URL or local path. remoteAddr, err := form.ParseRemoteAddr(ctx.User)
remoteAddr := form.CloneAddr
if strings.HasPrefix(form.CloneAddr, "http://") ||
strings.HasPrefix(form.CloneAddr, "https://") ||
strings.HasPrefix(form.CloneAddr, "git://") {
u, err := url.Parse(form.CloneAddr)
if err != nil { if err != nil {
if models.IsErrInvalidCloneAddr(err) {
addrErr := err.(models.ErrInvalidCloneAddr)
switch {
case addrErr.IsURLError:
ctx.APIError(422, "", err) ctx.APIError(422, "", err)
return case addrErr.IsPermissionDenied:
} ctx.APIError(422, "", "You are not allowed to import local repositories.")
if len(form.AuthUsername) > 0 || len(form.AuthPassword) > 0 { case addrErr.IsInvalidPath:
u.User = url.UserPassword(form.AuthUsername, form.AuthPassword)
}
remoteAddr = u.String()
} else if !com.IsDir(remoteAddr) {
ctx.APIError(422, "", "Invalid local path, it does not exist or not a directory.") ctx.APIError(422, "", "Invalid local path, it does not exist or not a directory.")
default:
ctx.APIError(500, "ParseRemoteAddr", "Unknown error type (ErrInvalidCloneAddr): "+err.Error())
}
} else {
ctx.APIError(500, "ParseRemoteAddr", err)
}
return return
} }

View file

@ -6,7 +6,6 @@ package repo
import ( import (
"fmt" "fmt"
"net/url"
"os" "os"
"path" "path"
"strings" "strings"
@ -164,26 +163,24 @@ func MigratePost(ctx *middleware.Context, form auth.MigrateRepoForm) {
return return
} }
// Remote address can be HTTP/HTTPS/Git URL or local path. remoteAddr, err := form.ParseRemoteAddr(ctx.User)
// Note: remember to change api/v1/repo.go: MigrateRepo
// FIXME: merge these two functions with better error handling
remoteAddr := form.CloneAddr
if strings.HasPrefix(form.CloneAddr, "http://") ||
strings.HasPrefix(form.CloneAddr, "https://") ||
strings.HasPrefix(form.CloneAddr, "git://") {
u, err := url.Parse(form.CloneAddr)
if err != nil { if err != nil {
if models.IsErrInvalidCloneAddr(err) {
ctx.Data["Err_CloneAddr"] = true ctx.Data["Err_CloneAddr"] = true
addrErr := err.(models.ErrInvalidCloneAddr)
switch {
case addrErr.IsURLError:
ctx.RenderWithErr(ctx.Tr("form.url_error"), MIGRATE, &form) ctx.RenderWithErr(ctx.Tr("form.url_error"), MIGRATE, &form)
return case addrErr.IsPermissionDenied:
} ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), MIGRATE, &form)
if len(form.AuthUsername) > 0 || len(form.AuthPassword) > 0 { case addrErr.IsInvalidPath:
u.User = url.UserPassword(form.AuthUsername, form.AuthPassword)
}
remoteAddr = u.String()
} else if !com.IsDir(remoteAddr) {
ctx.Data["Err_CloneAddr"] = true
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), MIGRATE, &form) ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), MIGRATE, &form)
default:
ctx.Handle(500, "Unknown error", err)
}
} else {
ctx.Handle(500, "ParseRemoteAddr", err)
}
return return
} }

View file

@ -1 +1 @@
0.6.23.1103 Beta 0.6.24.1103 Beta

View file

@ -72,7 +72,13 @@
<div class="inline field"> <div class="inline field">
<div class="ui checkbox"> <div class="ui checkbox">
<label><strong>{{.i18n.Tr "admin.users.allow_git_hook"}}</strong></label> <label><strong>{{.i18n.Tr "admin.users.allow_git_hook"}}</strong></label>
<input name="allow_git_hook" type="checkbox" {{if or .User.IsAdmin .User.AllowGitHook}}checked{{end}}> <input name="allow_git_hook" type="checkbox" {{if .User.CanEditGitHook}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label><strong>{{.i18n.Tr "admin.users.allow_import_local"}}</strong></label>
<input name="allow_import_local" type="checkbox" {{if .User.CanImportLocal}}checked{{end}}>
</div> </div>
</div> </div>

View file

@ -10,7 +10,7 @@
<a class="{{if .PageIsSettingsHooks}}active{{end}} item" href="{{.RepoLink}}/settings/hooks"> <a class="{{if .PageIsSettingsHooks}}active{{end}} item" href="{{.RepoLink}}/settings/hooks">
{{.i18n.Tr "repo.settings.hooks"}} {{.i18n.Tr "repo.settings.hooks"}}
</a> </a>
{{if or .SignedUser.AllowGitHook .SignedUser.IsAdmin}} {{if .SignedUser.CanEditGitHook}}
<a class="{{if .PageIsSettingsGitHooks}}active{{end}} item" href="{{.RepoLink}}/settings/hooks/git"> <a class="{{if .PageIsSettingsGitHooks}}active{{end}} item" href="{{.RepoLink}}/settings/hooks/git">
{{.i18n.Tr "repo.settings.githooks"}} {{.i18n.Tr "repo.settings.githooks"}}
</a> </a>