Skip to content

Commit 0e6b9ea

Browse files
authored
Take back control of hooks (#1006)
* git: delegate all server-side Git hooks (#1623) * create hooks directories * take control hooks back * fix lint * bug fixed and minor changes * fix imports style * fix migration scripts
1 parent 4f3880f commit 0e6b9ea

File tree

14 files changed

+279
-41
lines changed

14 files changed

+279
-41
lines changed

cmd/hook.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package cmd
6+
7+
import (
8+
"fmt"
9+
"os"
10+
11+
"code.gitea.io/gitea/models"
12+
13+
"github.com/urfave/cli"
14+
)
15+
16+
var (
17+
// CmdHook represents the available hooks sub-command.
18+
CmdHook = cli.Command{
19+
Name: "hook",
20+
Usage: "Delegate commands to corresponding Git hooks",
21+
Description: "This should only be called by Git",
22+
Flags: []cli.Flag{
23+
cli.StringFlag{
24+
Name: "config, c",
25+
Value: "custom/conf/app.ini",
26+
Usage: "Custom configuration file path",
27+
},
28+
},
29+
Subcommands: []cli.Command{
30+
subcmdHookPreReceive,
31+
subcmdHookUpadte,
32+
subcmdHookPostReceive,
33+
},
34+
}
35+
36+
subcmdHookPreReceive = cli.Command{
37+
Name: "pre-receive",
38+
Usage: "Delegate pre-receive Git hook",
39+
Description: "This command should only be called by Git",
40+
Action: runHookPreReceive,
41+
}
42+
subcmdHookUpadte = cli.Command{
43+
Name: "update",
44+
Usage: "Delegate update Git hook",
45+
Description: "This command should only be called by Git",
46+
Action: runHookUpdate,
47+
}
48+
subcmdHookPostReceive = cli.Command{
49+
Name: "post-receive",
50+
Usage: "Delegate post-receive Git hook",
51+
Description: "This command should only be called by Git",
52+
Action: runHookPostReceive,
53+
}
54+
)
55+
56+
func runHookPreReceive(c *cli.Context) error {
57+
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
58+
return nil
59+
}
60+
if err := setup("hooks/pre-receive.log"); err != nil {
61+
fail("Hook pre-receive init failed", fmt.Sprintf("setup: %v", err))
62+
}
63+
64+
return nil
65+
}
66+
67+
func runHookUpdate(c *cli.Context) error {
68+
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
69+
return nil
70+
}
71+
72+
if err := setup("hooks/update.log"); err != nil {
73+
fail("Hook update init failed", fmt.Sprintf("setup: %v", err))
74+
}
75+
76+
args := c.Args()
77+
if len(args) != 3 {
78+
fail("Arguments received are not equal to three", "Arguments received are not equal to three")
79+
} else if len(args[0]) == 0 {
80+
fail("First argument 'refName' is empty", "First argument 'refName' is empty")
81+
}
82+
83+
uuid := os.Getenv(envUpdateTaskUUID)
84+
if err := models.AddUpdateTask(&models.UpdateTask{
85+
UUID: uuid,
86+
RefName: args[0],
87+
OldCommitID: args[1],
88+
NewCommitID: args[2],
89+
}); err != nil {
90+
fail("Internal error", "Fail to add update task '%s': %v", uuid, err)
91+
}
92+
93+
return nil
94+
}
95+
96+
func runHookPostReceive(c *cli.Context) error {
97+
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
98+
return nil
99+
}
100+
101+
if err := setup("hooks/post-receive.log"); err != nil {
102+
fail("Hook post-receive init failed", fmt.Sprintf("setup: %v", err))
103+
}
104+
105+
return nil
106+
}

cmd/serve.go renamed to cmd/serv.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
const (
3131
accessDenied = "Repository does not exist or you do not have access"
3232
lfsAuthenticateVerb = "git-lfs-authenticate"
33+
envUpdateTaskUUID = "GITEA_UUID"
3334
)
3435

3536
// CmdServ represents the available serv sub-command.
@@ -170,7 +171,6 @@ func runServ(c *cli.Context) error {
170171

171172
var lfsVerb string
172173
if verb == lfsAuthenticateVerb {
173-
174174
if !setting.LFS.StartServer {
175175
fail("Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled")
176176
}
@@ -291,9 +291,7 @@ func runServ(c *cli.Context) error {
291291
}
292292

293293
//LFS token authentication
294-
295294
if verb == lfsAuthenticateVerb {
296-
297295
url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, repoUser.Name, repo.Name)
298296

299297
now := time.Now()
@@ -326,7 +324,7 @@ func runServ(c *cli.Context) error {
326324
}
327325

328326
uuid := gouuid.NewV4().String()
329-
os.Setenv("GITEA_UUID", uuid)
327+
os.Setenv(envUpdateTaskUUID, uuid)
330328
// Keep the old env variable name for backward compability
331329
os.Setenv("uuid", uuid)
332330

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func main() {
3030
app.Commands = []cli.Command{
3131
cmd.CmdWeb,
3232
cmd.CmdServ,
33-
cmd.CmdUpdate,
33+
cmd.CmdHook,
3434
cmd.CmdDump,
3535
cmd.CmdCert,
3636
cmd.CmdAdmin,

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ var migrations = []Migration{
8686
NewMigration("set protect branches updated with created", setProtectedBranchUpdatedWithCreated),
8787
// v18 -> v19
8888
NewMigration("add external login user", addExternalLoginUser),
89+
// v19 -> v20
90+
NewMigration("generate and migrate Git hooks", generateAndMigrateGitHooks),
8991
}
9092

9193
// Migrate database to current version

models/migrations/v18.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import (
1313
// ExternalLoginUser makes the connecting between some existing user and additional external login sources
1414
type ExternalLoginUser struct {
1515
ExternalID string `xorm:"NOT NULL"`
16-
UserID int64 `xorm:"NOT NULL"`
17-
LoginSourceID int64 `xorm:"NOT NULL"`
16+
UserID int64 `xorm:"NOT NULL"`
17+
LoginSourceID int64 `xorm:"NOT NULL"`
1818
}
1919

2020
func addExternalLoginUser(x *xorm.Engine) error {

models/migrations/v19.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"fmt"
9+
"io/ioutil"
10+
"os"
11+
"path/filepath"
12+
"strings"
13+
14+
"code.gitea.io/gitea/modules/setting"
15+
16+
"github.com/Unknwon/com"
17+
"github.com/go-xorm/xorm"
18+
)
19+
20+
func generateAndMigrateGitHooks(x *xorm.Engine) (err error) {
21+
type Repository struct {
22+
ID int64
23+
OwnerID int64
24+
Name string
25+
}
26+
type User struct {
27+
ID int64
28+
Name string
29+
}
30+
31+
var (
32+
hookNames = []string{"pre-receive", "update", "post-receive"}
33+
hookTpls = []string{
34+
fmt.Sprintf("#!/usr/bin/env %s\nORI_DIR=`pwd`\nSHELL_FOLDER=$(cd \"$(dirname \"$0\")\";pwd)\ncd \"$ORI_DIR\"\nfor i in `ls \"$SHELL_FOLDER/pre-receive.d\"`; do\n sh \"$SHELL_FOLDER/pre-receive.d/$i\"\ndone", setting.ScriptType),
35+
fmt.Sprintf("#!/usr/bin/env %s\nORI_DIR=`pwd`\nSHELL_FOLDER=$(cd \"$(dirname \"$0\")\";pwd)\ncd \"$ORI_DIR\"\nfor i in `ls \"$SHELL_FOLDER/update.d\"`; do\n sh \"$SHELL_FOLDER/update.d/$i\" $1 $2 $3\ndone", setting.ScriptType),
36+
fmt.Sprintf("#!/usr/bin/env %s\nORI_DIR=`pwd`\nSHELL_FOLDER=$(cd \"$(dirname \"$0\")\";pwd)\ncd \"$ORI_DIR\"\nfor i in `ls \"$SHELL_FOLDER/post-receive.d\"`; do\n sh \"$SHELL_FOLDER/post-receive.d/$i\"\ndone", setting.ScriptType),
37+
}
38+
giteaHookTpls = []string{
39+
fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' pre-receive\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
40+
fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' update $1 $2 $3\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
41+
fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' post-receive\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
42+
}
43+
)
44+
45+
return x.Where("id > 0").Iterate(new(Repository),
46+
func(idx int, bean interface{}) error {
47+
repo := bean.(*Repository)
48+
user := new(User)
49+
has, err := x.Where("id = ?", repo.OwnerID).Get(user)
50+
if err != nil {
51+
return fmt.Errorf("query owner of repository [repo_id: %d, owner_id: %d]: %v", repo.ID, repo.OwnerID, err)
52+
} else if !has {
53+
return nil
54+
}
55+
56+
repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(user.Name), strings.ToLower(repo.Name)) + ".git"
57+
hookDir := filepath.Join(repoPath, "hooks")
58+
59+
for i, hookName := range hookNames {
60+
oldHookPath := filepath.Join(hookDir, hookName)
61+
newHookPath := filepath.Join(hookDir, hookName+".d", "gitea")
62+
63+
if err = os.MkdirAll(filepath.Join(hookDir, hookName+".d"), os.ModePerm); err != nil {
64+
return fmt.Errorf("create hooks dir '%s': %v", filepath.Join(hookDir, hookName+".d"), err)
65+
}
66+
67+
// WARNING: Old server-side hooks will be moved to sub directory with the same name
68+
if hookName != "update" && com.IsExist(oldHookPath) {
69+
newPlace := filepath.Join(hookDir, hookName+".d", hookName)
70+
if err = os.Rename(oldHookPath, newPlace); err != nil {
71+
return fmt.Errorf("Remove old hook file '%s' to '%s': %v", oldHookPath, newPlace, err)
72+
}
73+
}
74+
75+
if err = ioutil.WriteFile(oldHookPath, []byte(hookTpls[i]), 0777); err != nil {
76+
return fmt.Errorf("write old hook file '%s': %v", oldHookPath, err)
77+
}
78+
79+
if err = ioutil.WriteFile(newHookPath, []byte(giteaHookTpls[i]), 0777); err != nil {
80+
return fmt.Errorf("write new hook file '%s': %v", oldHookPath, err)
81+
}
82+
}
83+
return nil
84+
})
85+
}

models/repo.go

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -831,20 +831,54 @@ func cleanUpMigrateGitConfig(configPath string) error {
831831
return nil
832832
}
833833

834-
func createUpdateHook(repoPath string) error {
835-
return git.SetUpdateHook(repoPath,
836-
fmt.Sprintf(tplUpdateHook, setting.ScriptType, "\""+setting.AppPath+"\"", setting.CustomConf))
834+
// createDelegateHooks creates all the hooks scripts for the repo
835+
func createDelegateHooks(repoPath string) (err error) {
836+
var (
837+
hookNames = []string{"pre-receive", "update", "post-receive"}
838+
hookTpls = []string{
839+
fmt.Sprintf("#!/usr/bin/env %s\nORI_DIR=`pwd`\nSHELL_FOLDER=$(cd \"$(dirname \"$0\")\";pwd)\ncd \"$ORI_DIR\"\nfor i in `ls \"$SHELL_FOLDER/pre-receive.d\"`; do\n sh \"$SHELL_FOLDER/pre-receive.d/$i\"\ndone", setting.ScriptType),
840+
fmt.Sprintf("#!/usr/bin/env %s\nORI_DIR=`pwd`\nSHELL_FOLDER=$(cd \"$(dirname \"$0\")\";pwd)\ncd \"$ORI_DIR\"\nfor i in `ls \"$SHELL_FOLDER/update.d\"`; do\n sh \"$SHELL_FOLDER/update.d/$i\" $1 $2 $3\ndone", setting.ScriptType),
841+
fmt.Sprintf("#!/usr/bin/env %s\nORI_DIR=`pwd`\nSHELL_FOLDER=$(cd \"$(dirname \"$0\")\";pwd)\ncd \"$ORI_DIR\"\nfor i in `ls \"$SHELL_FOLDER/post-receive.d\"`; do\n sh \"$SHELL_FOLDER/post-receive.d/$i\"\ndone", setting.ScriptType),
842+
}
843+
giteaHookTpls = []string{
844+
fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' pre-receive\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
845+
fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' update $1 $2 $3\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
846+
fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' post-receive\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
847+
}
848+
)
849+
850+
hookDir := filepath.Join(repoPath, "hooks")
851+
852+
for i, hookName := range hookNames {
853+
oldHookPath := filepath.Join(hookDir, hookName)
854+
newHookPath := filepath.Join(hookDir, hookName+".d", "gitea")
855+
856+
if err := os.MkdirAll(filepath.Join(hookDir, hookName+".d"), os.ModePerm); err != nil {
857+
return fmt.Errorf("create hooks dir '%s': %v", filepath.Join(hookDir, hookName+".d"), err)
858+
}
859+
860+
// WARNING: This will override all old server-side hooks
861+
if err = ioutil.WriteFile(oldHookPath, []byte(hookTpls[i]), 0777); err != nil {
862+
return fmt.Errorf("write old hook file '%s': %v", oldHookPath, err)
863+
}
864+
865+
if err = ioutil.WriteFile(newHookPath, []byte(giteaHookTpls[i]), 0777); err != nil {
866+
return fmt.Errorf("write new hook file '%s': %v", newHookPath, err)
867+
}
868+
}
869+
870+
return nil
837871
}
838872

839873
// CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors.
840874
func CleanUpMigrateInfo(repo *Repository) (*Repository, error) {
841875
repoPath := repo.RepoPath()
842-
if err := createUpdateHook(repoPath); err != nil {
843-
return repo, fmt.Errorf("createUpdateHook: %v", err)
876+
if err := createDelegateHooks(repoPath); err != nil {
877+
return repo, fmt.Errorf("createDelegateHooks: %v", err)
844878
}
845879
if repo.HasWiki() {
846-
if err := createUpdateHook(repo.WikiPath()); err != nil {
847-
return repo, fmt.Errorf("createUpdateHook (wiki): %v", err)
880+
if err := createDelegateHooks(repo.WikiPath()); err != nil {
881+
return repo, fmt.Errorf("createDelegateHooks.(wiki): %v", err)
848882
}
849883
}
850884

@@ -994,8 +1028,8 @@ func initRepository(e Engine, repoPath string, u *User, repo *Repository, opts C
9941028
// Init bare new repository.
9951029
if err = git.InitRepository(repoPath, true); err != nil {
9961030
return fmt.Errorf("InitRepository: %v", err)
997-
} else if err = createUpdateHook(repoPath); err != nil {
998-
return fmt.Errorf("createUpdateHook: %v", err)
1031+
} else if err = createDelegateHooks(repoPath); err != nil {
1032+
return fmt.Errorf("createDelegateHooks: %v", err)
9991033
}
10001034

10011035
tmpDir := filepath.Join(os.TempDir(), "gitea-"+repo.Name+"-"+com.ToStr(time.Now().Nanosecond()))
@@ -2009,15 +2043,16 @@ func ReinitMissingRepositories() error {
20092043
return nil
20102044
}
20112045

2012-
// RewriteRepositoryUpdateHook rewrites all repositories' update hook.
2013-
func RewriteRepositoryUpdateHook() error {
2014-
return x.
2015-
Where("id > 0").
2016-
Iterate(new(Repository),
2017-
func(idx int, bean interface{}) error {
2018-
repo := bean.(*Repository)
2019-
return createUpdateHook(repo.RepoPath())
2020-
})
2046+
// SyncRepositoryHooks rewrites all repositories' pre-receive, update and post-receive hooks
2047+
// to make sure the binary and custom conf path are up-to-date.
2048+
func SyncRepositoryHooks() error {
2049+
return x.Where("id > 0").Iterate(new(Repository),
2050+
func(idx int, bean interface{}) error {
2051+
if err := createDelegateHooks(bean.(*Repository).RepoPath()); err != nil {
2052+
return fmt.Errorf("SyncRepositoryHook: %v", err)
2053+
}
2054+
return nil
2055+
})
20212056
}
20222057

20232058
// Prevent duplicate running tasks.
@@ -2345,8 +2380,8 @@ func ForkRepository(u *User, oldRepo *Repository, name, desc string) (_ *Reposit
23452380
return nil, fmt.Errorf("git update-server-info: %v", stderr)
23462381
}
23472382

2348-
if err = createUpdateHook(repoPath); err != nil {
2349-
return nil, fmt.Errorf("createUpdateHook: %v", err)
2383+
if err = createDelegateHooks(repoPath); err != nil {
2384+
return nil, fmt.Errorf("createDelegateHooks: %v", err)
23502385
}
23512386

23522387
//Commit repo to get Fork ID

models/wiki.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ func (repo *Repository) InitWiki() error {
6969

7070
if err := git.InitRepository(repo.WikiPath(), true); err != nil {
7171
return fmt.Errorf("InitRepository: %v", err)
72-
} else if err = createUpdateHook(repo.WikiPath()); err != nil {
73-
return fmt.Errorf("createUpdateHook: %v", err)
72+
} else if err = createDelegateHooks(repo.WikiPath()); err != nil {
73+
return fmt.Errorf("createDelegateHooks: %v", err)
7474
}
7575
return nil
7676
}

options/locale/locale_en-US.ini

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,8 +1003,8 @@ dashboard.git_gc_repos = Do garbage collection on repositories
10031003
dashboard.git_gc_repos_success = All repositories have done garbage collection successfully.
10041004
dashboard.resync_all_sshkeys = Rewrite '.ssh/authorized_keys' file (caution: non-Gitea keys will be lost)
10051005
dashboard.resync_all_sshkeys_success = All public keys have been rewritten successfully.
1006-
dashboard.resync_all_update_hooks = Rewrite all update hook of repositories (needed when custom config path is changed)
1007-
dashboard.resync_all_update_hooks_success = All repositories' update hook have been rewritten successfully.
1006+
dashboard.resync_all_hooks = Resync pre-receive, update and post-receive hooks of all repositories.
1007+
dashboard.resync_all_hooks_success = All repositories' pre-receive, update and post-receive hooks have been resynced successfully.
10081008
dashboard.reinit_missing_repos = Reinitialize all repository records that lost Git files
10091009
dashboard.reinit_missing_repos_success = All repository records that lost Git files have been reinitialized successfully.
10101010

routers/admin/admin.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ func Dashboard(ctx *context.Context) {
152152
success = ctx.Tr("admin.dashboard.resync_all_sshkeys_success")
153153
err = models.RewriteAllPublicKeys()
154154
case syncRepositoryUpdateHook:
155-
success = ctx.Tr("admin.dashboard.resync_all_update_hooks_success")
156-
err = models.RewriteRepositoryUpdateHook()
155+
success = ctx.Tr("admin.dashboard.resync_all_hooks_success")
156+
err = models.SyncRepositoryHooks()
157157
case reinitMissingRepository:
158158
success = ctx.Tr("admin.dashboard.reinit_missing_repos_success")
159159
err = models.ReinitMissingRepositories()

0 commit comments

Comments
 (0)