diff --git a/README.md b/README.md
index b7ff264e35..826d9b9e42 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@ Gogs(Go Git Service) is a painless self-hosted Git Service written in Go.
![Demo](https://gowalker.org/public/gogs_demo.gif)
-##### Current version: 0.5.4 Beta
+##### Current version: 0.5.5 Beta
### NOTICES
@@ -44,7 +44,7 @@ The goal of this project is to make the easiest, fastest and most painless way t
- Slack webhook integration
- Supports MySQL, PostgreSQL and SQLite3
- Social account login(GitHub, Google, QQ, Weibo)
-- Multi-language support(English, Chinese, Germany, French etc.)
+- Multi-language support(English, Chinese, Germany, French, Dutch etc.)
## System Requirements
diff --git a/README_ZH.md b/README_ZH.md
index d704053f22..3da3b6be76 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -5,7 +5,7 @@ Gogs(Go Git Service) 是一个基于 Go 语言的自助 Git 服务。
![Demo](https://gowalker.org/public/gogs_demo.gif)
-##### 当前版本:0.5.4 Beta
+##### 当前版本:0.5.5 Beta
## 开发目的
@@ -35,7 +35,7 @@ Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自
- Slack Web 钩子集成
- 支持 MySQL、PostgreSQL 以及 SQLite3 数据库
- 社交帐号登录(GitHub、Google、QQ、微博)
-- 多语言支持(英文、简体中文、德语、法语等等)
+- 多语言支持(英文、简体中文、德语、法语、荷兰语等等)
## 系统要求
diff --git a/cmd/web.go b/cmd/web.go
index 72a58bc995..810e36d3bb 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -313,6 +313,12 @@ func runWeb(*cli.Context) {
r.Get("/hooks/:id", repo.WebHooksEdit)
r.Post("/hooks/gogs/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost)
r.Post("/hooks/slack/:id", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksEditPost)
+
+ m.Group("/hooks/git", func(r *macaron.Router) {
+ r.Get("", repo.GitHooks)
+ r.Get("/:name", repo.GitHooksEdit)
+ r.Post("/:name", repo.GitHooksEditPost)
+ }, middleware.GitHookService())
})
}, reqSignIn, middleware.RepoAssignment(true), reqTrueOwner)
diff --git a/conf/app.ini b/conf/app.ini
index 224f45dd62..1ea92e9f3b 100644
--- a/conf/app.ini
+++ b/conf/app.ini
@@ -70,6 +70,8 @@ ENABLE_CACHE_AVATAR = false
ENABLE_NOTIFY_MAIL = false
; More detail: https://github.com/gogits/gogs/issues/165
ENABLE_REVERSE_PROXY_AUTHENTICATION = false
+; Repository Git hooks
+ENABLE_GIT_HOOKS = false
[webhook]
; Cron task interval in minutes
diff --git a/conf/locale/locale_en-US.ini b/conf/locale/locale_en-US.ini
index 8e68fb9803..4fc8c359ff 100644
--- a/conf/locale/locale_en-US.ini
+++ b/conf/locale/locale_en-US.ini
@@ -287,6 +287,7 @@ settings = Settings
settings.options = Options
settings.collaboration = Collaboration
settings.hooks = Webhooks
+settings.githooks = Git Hooks
settings.deploy_keys = Deploy Keys
settings.basic_settings = Basic Settings
settings.danger_zone = Danger Zone
@@ -310,6 +311,11 @@ settings.add_collaborator_success = New collaborator has been added.
settings.remove_collaborator_success = Collaborator has been removed.
settings.add_webhook = Add Webhook
settings.hooks_desc = Webhooks allow external services to be notified when certain events happen on Gogs. When the specified events happen, we'll send a POST request to each of the URLs you provide. Learn more in our Webhooks Guide.
+settings.githooks_desc = Git Hooks are powered by Git itself, you can edit files of supported hooks in the list below to apply custom operations.
+settings.githook_edit_desc = If hook is not active, sample content will be presented. Leave content to be blank will disable this hook.
+settings.githook_name = Hook Name
+settings.githook_content = Hook Content
+settings.update_githook = Update Hook
settings.remove_hook_success = Webhook has been removed.
settings.add_webhook_desc = We’ll send a POST
request to the URL below with details of any subscribed events. You can also specify which data format you'd like to receive (JSON, x-www-form-urlencoded
, etc). More information can be found in Webhooks Guide.
settings.payload_url = Payload URL
diff --git a/conf/locale/locale_zh-CN.ini b/conf/locale/locale_zh-CN.ini
index 360bf4bcc6..dc4548487b 100644
--- a/conf/locale/locale_zh-CN.ini
+++ b/conf/locale/locale_zh-CN.ini
@@ -287,6 +287,7 @@ settings = 仓库设置
settings.options = 基本设置
settings.collaboration = 管理协作者
settings.hooks = 管理 Web 钩子
+settings.githooks = 管理 Git 钩子
settings.deploy_keys = 管理部署密钥
settings.basic_settings = 基本设置
settings.danger_zone = 危险操作区
@@ -312,6 +313,11 @@ settings.add_webhook = 添加 Web 钩子
settings.hooks_desc = Web 钩子允许您设定在 Gogs 上发生指定事件时对指定 URL 发送 POST 通知。查看 Webhooks 文档 获取更多信息。
settings.remove_hook_success = Web 钩子删除成功!
settings.add_webhook_desc = 我们会通过 POST
请求将订阅事件信息发送至向指定 URL 地址。您可以设置不同的数据接收方式(JSON 或 x-www-form-urlencoded
)。 请查阅 Webhooks 文档 获取更多信息。
+settings.githooks_desc = Git 钩子是由 Git 本身提供的功能,以下为 Gogs 所支持的钩子列表。
+settings.githook_edit_desc = 如果钩子未启动,则会显示样例文件中的内容。如果想要删除某个钩子,则提交空白文本即可。
+settings.githook_name = 钩子名称
+settings.githook_content = 钩子文本
+settings.update_githook = 更新钩子设置
settings.payload_url = 推送地址
settings.content_type = 数据格式
settings.secret = 密钥文本
diff --git a/gogs.go b/gogs.go
index c0f1d1e26a..43705061c9 100644
--- a/gogs.go
+++ b/gogs.go
@@ -17,7 +17,7 @@ import (
"github.com/gogits/gogs/modules/setting"
)
-const APP_VER = "0.5.4.1005 Beta"
+const APP_VER = "0.5.5.1006 Beta"
func init() {
runtime.GOMAXPROCS(runtime.NumCPU())
diff --git a/modules/git/hooks.go b/modules/git/hooks.go
new file mode 100644
index 0000000000..b8d15e5e75
--- /dev/null
+++ b/modules/git/hooks.go
@@ -0,0 +1,111 @@
+// 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 git
+
+import (
+ "errors"
+ "io/ioutil"
+ "os"
+ "path"
+ "strings"
+)
+
+// hookNames is a list of Git hooks' name that are supported.
+var hookNames = []string{
+ "pre-applypatch",
+ "applypatch-msg",
+ "prepare-commit-msg",
+ "commit-msg",
+ "pre-commit",
+ "pre-rebase",
+ "post-commit",
+ "post-receive",
+ "post-update",
+}
+
+var (
+ ErrNotValidHook = errors.New("not a valid Git hook")
+)
+
+// IsValidHookName returns true if given name is a valid Git hook.
+func IsValidHookName(name string) bool {
+ for _, hn := range hookNames {
+ if hn == name {
+ return true
+ }
+ }
+ return false
+}
+
+// Hook represents a Git hook.
+type Hook struct {
+ name string
+ IsActive bool // Indicates whether repository has this hook.
+ Content string // Content of hook if it's active.
+ Sample string // Sample content from Git.
+ path string // Hook file path.
+}
+
+// GetHook returns a Git hook by given name and repository.
+func GetHook(repoPath, name string) (*Hook, error) {
+ if !IsValidHookName(name) {
+ return nil, ErrNotValidHook
+ }
+ h := &Hook{
+ name: name,
+ path: path.Join(repoPath, "hooks", name),
+ }
+ if isFile(h.path) {
+ data, err := ioutil.ReadFile(h.path)
+ if err != nil {
+ return nil, err
+ }
+ h.IsActive = true
+ h.Content = string(data)
+ } else if isFile(h.path + ".sample") {
+ data, err := ioutil.ReadFile(h.path + ".sample")
+ if err != nil {
+ return nil, err
+ }
+ h.Sample = string(data)
+ }
+ return h, nil
+}
+
+func (h *Hook) Name() string {
+ return h.name
+}
+
+// Update updates hook settings.
+func (h *Hook) Update() error {
+ if len(strings.TrimSpace(h.Content)) == 0 {
+ return os.Remove(h.path)
+ }
+ return ioutil.WriteFile(h.path, []byte(h.Content), os.ModePerm)
+}
+
+// ListHooks returns a list of Git hooks of given repository.
+func ListHooks(repoPath string) (_ []*Hook, err error) {
+ if !isDir(path.Join(repoPath, "hooks")) {
+ return nil, errors.New("hooks path does not exist")
+ }
+
+ hooks := make([]*Hook, len(hookNames))
+ for i, name := range hookNames {
+ hooks[i], err = GetHook(repoPath, name)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return hooks, nil
+}
+
+func (repo *Repository) GetHook(name string) (*Hook, error) {
+ return GetHook(repo.Path, name)
+}
+
+func (repo *Repository) Hooks() ([]*Hook, error) {
+ return ListHooks(repo.Path)
+}
diff --git a/modules/git/utils.go b/modules/git/utils.go
index 26eef23191..6abbca557b 100644
--- a/modules/git/utils.go
+++ b/modules/git/utils.go
@@ -7,6 +7,7 @@ package git
import (
"bytes"
"container/list"
+ "os"
"path/filepath"
"strings"
)
@@ -46,3 +47,23 @@ func RefEndName(refStr string) string {
func filepathFromSHA1(rootdir, sha1 string) string {
return filepath.Join(rootdir, "objects", sha1[:2], sha1[2:])
}
+
+// isDir returns true if given path is a directory,
+// or returns false when it's a file or does not exist.
+func isDir(dir string) bool {
+ f, e := os.Stat(dir)
+ if e != nil {
+ return false
+ }
+ return f.IsDir()
+}
+
+// isFile returns true if given path is a file,
+// or returns false when it's a directory or does not exist.
+func isFile(filePath string) bool {
+ f, e := os.Stat(filePath)
+ if e != nil {
+ return false
+ }
+ return !f.IsDir()
+}
diff --git a/modules/middleware/repo.go b/modules/middleware/repo.go
index c6250f6d59..78af58eac8 100644
--- a/modules/middleware/repo.go
+++ b/modules/middleware/repo.go
@@ -308,3 +308,13 @@ func RequireTrueOwner() macaron.Handler {
}
}
}
+
+// GitHookService checks if repsitory Git hooks service has been enabled.
+func GitHookService() macaron.Handler {
+ return func(ctx *Context) {
+ if !setting.Service.EnableGitHooks {
+ ctx.Handle(404, "GitHookService", nil)
+ return
+ }
+ }
+}
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 67e48108d9..b8fc4dec2e 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -275,6 +275,7 @@ var Service struct {
LdapAuth bool
ActiveCodeLives int
ResetPwdCodeLives int
+ EnableGitHooks bool
}
func newService() {
@@ -284,6 +285,7 @@ func newService() {
Service.RequireSignInView = Cfg.MustBool("service", "REQUIRE_SIGNIN_VIEW")
Service.EnableCacheAvatar = Cfg.MustBool("service", "ENABLE_CACHE_AVATAR")
Service.EnableReverseProxyAuth = Cfg.MustBool("service", "ENABLE_REVERSE_PROXY_AUTHENTICATION")
+ Service.EnableGitHooks = Cfg.MustBool("service", "ENABLE_GIT_HOOKS")
}
var logLevels = map[string]string{
diff --git a/public/ng/css/ui.css b/public/ng/css/ui.css
index 9c3c8ded5d..3ea6fcd48c 100644
--- a/public/ng/css/ui.css
+++ b/public/ng/css/ui.css
@@ -480,6 +480,10 @@ dt {
.ipt-large {
font-size: 14.4px;
}
+.ipt-textarea {
+ height: auto !important;
+ width: auto;
+}
.ipt-disabled,
input[disabled] {
background-color: #f2f2f2 !important;
diff --git a/public/ng/less/ui/form.less b/public/ng/less/ui/form.less
index 4a681994de..b3de4273c8 100644
--- a/public/ng/less/ui/form.less
+++ b/public/ng/less/ui/form.less
@@ -116,25 +116,24 @@
}
// input form elements
-
.ipt {
- &:focus {
- border-color: @iptFocusBorderColor;
- }
+ &:focus {
+ border-color: @iptFocusBorderColor;
+ }
}
-
.ipt-radius {
- border-radius: .25em;
+ border-radius: .25em;
}
-
.ipt-small {
- font-size: .8*@baseFontSize;
+ font-size: .8*@baseFontSize;
}
-
.ipt-large {
- font-size: 1.2*@baseFontSize;
+ font-size: 1.2*@baseFontSize;
+}
+.ipt-textarea {
+ height: auto !important;
+ width: auto;
}
-
.ipt-disabled,
input[disabled] {
background-color: @iptDisabledColor !important;
@@ -144,14 +143,12 @@ input[disabled] {
color: #888;
cursor: not-allowed;
}
-
.ipt-readonly,
input[readonly] {
&:focus {
background-color: @iptDisabledColor !important;
}
}
-
.ipt-error {
border-color: @iptErrorBorderColor !important;
background-color: @iptErrorFocusColor !important;
diff --git a/routers/repo/setting.go b/routers/repo/setting.go
index 48089787fe..0e58029ec3 100644
--- a/routers/repo/setting.go
+++ b/routers/repo/setting.go
@@ -16,6 +16,7 @@ import (
"github.com/gogits/gogs/models"
"github.com/gogits/gogs/modules/auth"
"github.com/gogits/gogs/modules/base"
+ "github.com/gogits/gogs/modules/git"
"github.com/gogits/gogs/modules/log"
"github.com/gogits/gogs/modules/mailer"
"github.com/gogits/gogs/modules/middleware"
@@ -26,6 +27,8 @@ const (
SETTINGS_OPTIONS base.TplName = "repo/settings/options"
COLLABORATION base.TplName = "repo/settings/collaboration"
HOOKS base.TplName = "repo/settings/hooks"
+ GITHOOKS base.TplName = "repo/settings/githooks"
+ GITHOOK_EDIT base.TplName = "repo/settings/githook_edit"
HOOK_NEW base.TplName = "repo/settings/hook_new"
ORG_HOOK_NEW base.TplName = "org/settings/hook_new"
)
@@ -591,3 +594,54 @@ func getOrgRepoCtx(ctx *middleware.Context) (*OrgRepoCtx, error) {
return &OrgRepoCtx{}, errors.New("Unable to set OrgRepo context")
}
}
+
+func GitHooks(ctx *middleware.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsGitHooks"] = true
+
+ hooks, err := ctx.Repo.GitRepo.Hooks()
+ if err != nil {
+ ctx.Handle(500, "Hooks", err)
+ return
+ }
+ ctx.Data["Hooks"] = hooks
+
+ ctx.HTML(200, GITHOOKS)
+}
+
+func GitHooksEdit(ctx *middleware.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.settings")
+ ctx.Data["PageIsSettingsGitHooks"] = true
+
+ name := ctx.Params(":name")
+ hook, err := ctx.Repo.GitRepo.GetHook(name)
+ if err != nil {
+ if err == git.ErrNotValidHook {
+ ctx.Handle(404, "GetHook", err)
+ } else {
+ ctx.Handle(500, "GetHook", err)
+ }
+ return
+ }
+ ctx.Data["Hook"] = hook
+ ctx.HTML(200, GITHOOK_EDIT)
+}
+
+func GitHooksEditPost(ctx *middleware.Context) {
+ name := ctx.Params(":name")
+ hook, err := ctx.Repo.GitRepo.GetHook(name)
+ if err != nil {
+ if err == git.ErrNotValidHook {
+ ctx.Handle(404, "GetHook", err)
+ } else {
+ ctx.Handle(500, "GetHook", err)
+ }
+ return
+ }
+ hook.Content = ctx.Query("content")
+ if err = hook.Update(); err != nil {
+ ctx.Handle(500, "hook.Update", err)
+ return
+ }
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks/git")
+}
diff --git a/templates/.VERSION b/templates/.VERSION
index 7760f35b0d..e551dcfe40 100644
--- a/templates/.VERSION
+++ b/templates/.VERSION
@@ -1 +1 @@
-0.5.4.1005 Beta
\ No newline at end of file
+0.5.5.1006 Beta
\ No newline at end of file
diff --git a/templates/repo/settings/githook_edit.tmpl b/templates/repo/settings/githook_edit.tmpl
new file mode 100644
index 0000000000..23fc26e36e
--- /dev/null
+++ b/templates/repo/settings/githook_edit.tmpl
@@ -0,0 +1,41 @@
+{{template "ng/base/head" .}}
+{{template "ng/base/header" .}}
+