Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Take back control of hooks #1006

Merged
merged 7 commits into from
Feb 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions cmd/hook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2017 The Gitea 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 cmd

import (
"fmt"
"os"

"code.gitea.io/gitea/models"

"github.com/urfave/cli"
)

var (
// CmdHook represents the available hooks sub-command.
CmdHook = cli.Command{
Name: "hook",
Usage: "Delegate commands to corresponding Git hooks",
Description: "This should only be called by Git",
Flags: []cli.Flag{
cli.StringFlag{
Name: "config, c",
Value: "custom/conf/app.ini",
Usage: "Custom configuration file path",
},
},
Subcommands: []cli.Command{
subcmdHookPreReceive,
subcmdHookUpadte,
subcmdHookPostReceive,
},
}

subcmdHookPreReceive = cli.Command{
Name: "pre-receive",
Usage: "Delegate pre-receive Git hook",
Description: "This command should only be called by Git",
Action: runHookPreReceive,
}
subcmdHookUpadte = cli.Command{
Name: "update",
Usage: "Delegate update Git hook",
Description: "This command should only be called by Git",
Action: runHookUpdate,
}
subcmdHookPostReceive = cli.Command{
Name: "post-receive",
Usage: "Delegate post-receive Git hook",
Description: "This command should only be called by Git",
Action: runHookPostReceive,
}
)

func runHookPreReceive(c *cli.Context) error {
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
return nil
}
if err := setup("hooks/pre-receive.log"); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function name 'setup' is a bit of vague as to what it actually does. I guess it's about setting up a logger?
But I don't understand why it has anything to do with the database engine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setup is to initialize the command, this function is for serval commands which the different is the log place.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it's resulted from factoring out the common 'preparing' part of all the commands. Maybe the Before callback is a suitable place for it.

https://sourcegraph.com/github.com/lunny/gitea@f132edf80f2de3b9335e99eec7d23abdfcc6670f/-/blob/vendor/github.com/urfave/cli/command.go#L32:2-32:8

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before should be the same actions, but this time we have a parameter - log name.

Copy link
Contributor

@typeless typeless Feb 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Before callback has the context parameter which has the needed (I guess) information like the command name to construct the path of the log file, if I wasn't misunderstanding something

Actually, I am not sure if putting it in Before is better or not. But there is the possibility. 😉

fail("Hook pre-receive init failed", fmt.Sprintf("setup: %v", err))
}

return nil
}

func runHookUpdate(c *cli.Context) error {
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
Copy link
Contributor

@typeless typeless Feb 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why the check is needed, with the nearby context.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the SSH_ORIGINAL_COMMAND is not set, that's because it is called by shell on manually.

return nil
}

if err := setup("hooks/update.log"); err != nil {
fail("Hook update init failed", fmt.Sprintf("setup: %v", err))
}

args := c.Args()
if len(args) != 3 {
fail("Arguments received are not equal to three", "Arguments received are not equal to three")
} else if len(args[0]) == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose the two conditions are not mutually exclusive? Not sure if I understand it correctly, but the use of else if seems suspicious.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Here I just move codes from update.go to hook.go. I think it's no problem. Suppose the command hook update "" a b. The refName is empty.

fail("First argument 'refName' is empty", "First argument 'refName' is empty")
}

uuid := os.Getenv(envUpdateTaskUUID)
if err := models.AddUpdateTask(&models.UpdateTask{
UUID: uuid,
RefName: args[0],
OldCommitID: args[1],
NewCommitID: args[2],
}); err != nil {
fail("Internal error", "Fail to add update task '%s': %v", uuid, err)
}

return nil
}

func runHookPostReceive(c *cli.Context) error {
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
return nil
}

if err := setup("hooks/post-receive.log"); err != nil {
fail("Hook post-receive init failed", fmt.Sprintf("setup: %v", err))
}

return nil
}
6 changes: 2 additions & 4 deletions cmd/serve.go → cmd/serv.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
const (
accessDenied = "Repository does not exist or you do not have access"
lfsAuthenticateVerb = "git-lfs-authenticate"
envUpdateTaskUUID = "GITEA_UUID"
)

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

var lfsVerb string
if verb == lfsAuthenticateVerb {

if !setting.LFS.StartServer {
fail("Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled")
}
Expand Down Expand Up @@ -291,9 +291,7 @@ func runServ(c *cli.Context) error {
}

//LFS token authentication

if verb == lfsAuthenticateVerb {

url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, repoUser.Name, repo.Name)

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

uuid := gouuid.NewV4().String()
os.Setenv("GITEA_UUID", uuid)
os.Setenv(envUpdateTaskUUID, uuid)
// Keep the old env variable name for backward compability
os.Setenv("uuid", uuid)

Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func main() {
app.Commands = []cli.Command{
cmd.CmdWeb,
cmd.CmdServ,
cmd.CmdUpdate,
cmd.CmdHook,
cmd.CmdDump,
cmd.CmdCert,
cmd.CmdAdmin,
Expand Down
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ var migrations = []Migration{
NewMigration("set protect branches updated with created", setProtectedBranchUpdatedWithCreated),
// v18 -> v19
NewMigration("add external login user", addExternalLoginUser),
// v19 -> v20
NewMigration("generate and migrate Git hooks", generateAndMigrateGitHooks),
}

// Migrate database to current version
Expand Down
4 changes: 2 additions & 2 deletions models/migrations/v18.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import (
// ExternalLoginUser makes the connecting between some existing user and additional external login sources
type ExternalLoginUser struct {
ExternalID string `xorm:"NOT NULL"`
UserID int64 `xorm:"NOT NULL"`
LoginSourceID int64 `xorm:"NOT NULL"`
UserID int64 `xorm:"NOT NULL"`
LoginSourceID int64 `xorm:"NOT NULL"`
}

func addExternalLoginUser(x *xorm.Engine) error {
Expand Down
85 changes: 85 additions & 0 deletions models/migrations/v19.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2017 The Gitea 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 migrations

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"

"code.gitea.io/gitea/modules/setting"

"github.com/Unknwon/com"
"github.com/go-xorm/xorm"
)

func generateAndMigrateGitHooks(x *xorm.Engine) (err error) {
type Repository struct {
ID int64
OwnerID int64
Name string
}
type User struct {
ID int64
Name string
}

var (
hookNames = []string{"pre-receive", "update", "post-receive"}
hookTpls = []string{
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),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's late but still want to comment it. You don't need the quotes for the ls and you don't need to call ls. Just something like for I in folder/*; do done. Beside that I would use source instead of sh to execute the scripts, than you can also define env variables in scripts that can be used in another script later on

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you are right since I'm not familiar with shell script. We also have runServe could define env variables before these script to be executed. So it's no need here to put another one.

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),
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),
}
giteaHookTpls = []string{
fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' pre-receive\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' update $1 $2 $3\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' post-receive\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
}
)

return x.Where("id > 0").Iterate(new(Repository),
func(idx int, bean interface{}) error {
repo := bean.(*Repository)
user := new(User)
has, err := x.Where("id = ?", repo.OwnerID).Get(user)
if err != nil {
return fmt.Errorf("query owner of repository [repo_id: %d, owner_id: %d]: %v", repo.ID, repo.OwnerID, err)
} else if !has {
return nil
}

repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(user.Name), strings.ToLower(repo.Name)) + ".git"
hookDir := filepath.Join(repoPath, "hooks")

for i, hookName := range hookNames {
oldHookPath := filepath.Join(hookDir, hookName)
newHookPath := filepath.Join(hookDir, hookName+".d", "gitea")

if err = os.MkdirAll(filepath.Join(hookDir, hookName+".d"), os.ModePerm); err != nil {
return fmt.Errorf("create hooks dir '%s': %v", filepath.Join(hookDir, hookName+".d"), err)
}

// WARNING: Old server-side hooks will be moved to sub directory with the same name
if hookName != "update" && com.IsExist(oldHookPath) {
newPlace := filepath.Join(hookDir, hookName+".d", hookName)
if err = os.Rename(oldHookPath, newPlace); err != nil {
return fmt.Errorf("Remove old hook file '%s' to '%s': %v", oldHookPath, newPlace, err)
}
}

if err = ioutil.WriteFile(oldHookPath, []byte(hookTpls[i]), 0777); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is 0777 necessary? Least privilege is prefered in general.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It seems 0755 is enough.

Copy link
Contributor

@typeless typeless Feb 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If 0700 is enough then 0700 is better. I cannot think of a scenario where 'others' need the permissions to R/W it.

Edit: Okay, it would be much convenient when debugging 😆

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. But I ls -l the repository's root dir, all the permission is 755. Don't know why.

return fmt.Errorf("write old hook file '%s': %v", oldHookPath, err)
}

if err = ioutil.WriteFile(newHookPath, []byte(giteaHookTpls[i]), 0777); err != nil {
return fmt.Errorf("write new hook file '%s': %v", oldHookPath, err)
}
}
return nil
})
}
75 changes: 55 additions & 20 deletions models/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -831,20 +831,54 @@ func cleanUpMigrateGitConfig(configPath string) error {
return nil
}

func createUpdateHook(repoPath string) error {
return git.SetUpdateHook(repoPath,
fmt.Sprintf(tplUpdateHook, setting.ScriptType, "\""+setting.AppPath+"\"", setting.CustomConf))
// createDelegateHooks creates all the hooks scripts for the repo
func createDelegateHooks(repoPath string) (err error) {
var (
hookNames = []string{"pre-receive", "update", "post-receive"}
hookTpls = []string{
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),
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),
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),
}
giteaHookTpls = []string{
fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' pre-receive\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' update $1 $2 $3\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' post-receive\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
}
)

hookDir := filepath.Join(repoPath, "hooks")

for i, hookName := range hookNames {
oldHookPath := filepath.Join(hookDir, hookName)
newHookPath := filepath.Join(hookDir, hookName+".d", "gitea")

if err := os.MkdirAll(filepath.Join(hookDir, hookName+".d"), os.ModePerm); err != nil {
return fmt.Errorf("create hooks dir '%s': %v", filepath.Join(hookDir, hookName+".d"), err)
}

// WARNING: This will override all old server-side hooks
if err = ioutil.WriteFile(oldHookPath, []byte(hookTpls[i]), 0777); err != nil {
return fmt.Errorf("write old hook file '%s': %v", oldHookPath, err)
}

if err = ioutil.WriteFile(newHookPath, []byte(giteaHookTpls[i]), 0777); err != nil {
return fmt.Errorf("write new hook file '%s': %v", newHookPath, err)
}
}

return nil
}

// CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors.
func CleanUpMigrateInfo(repo *Repository) (*Repository, error) {
repoPath := repo.RepoPath()
if err := createUpdateHook(repoPath); err != nil {
return repo, fmt.Errorf("createUpdateHook: %v", err)
if err := createDelegateHooks(repoPath); err != nil {
return repo, fmt.Errorf("createDelegateHooks: %v", err)
}
if repo.HasWiki() {
if err := createUpdateHook(repo.WikiPath()); err != nil {
return repo, fmt.Errorf("createUpdateHook (wiki): %v", err)
if err := createDelegateHooks(repo.WikiPath()); err != nil {
return repo, fmt.Errorf("createDelegateHooks.(wiki): %v", err)
}
}

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

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

// RewriteRepositoryUpdateHook rewrites all repositories' update hook.
func RewriteRepositoryUpdateHook() error {
return x.
Where("id > 0").
Iterate(new(Repository),
func(idx int, bean interface{}) error {
repo := bean.(*Repository)
return createUpdateHook(repo.RepoPath())
})
// SyncRepositoryHooks rewrites all repositories' pre-receive, update and post-receive hooks
// to make sure the binary and custom conf path are up-to-date.
func SyncRepositoryHooks() error {
return x.Where("id > 0").Iterate(new(Repository),
func(idx int, bean interface{}) error {
if err := createDelegateHooks(bean.(*Repository).RepoPath()); err != nil {
return fmt.Errorf("SyncRepositoryHook: %v", err)
}
return nil
})
}

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

if err = createUpdateHook(repoPath); err != nil {
return nil, fmt.Errorf("createUpdateHook: %v", err)
if err = createDelegateHooks(repoPath); err != nil {
return nil, fmt.Errorf("createDelegateHooks: %v", err)
}

//Commit repo to get Fork ID
Expand Down
4 changes: 2 additions & 2 deletions models/wiki.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ func (repo *Repository) InitWiki() error {

if err := git.InitRepository(repo.WikiPath(), true); err != nil {
return fmt.Errorf("InitRepository: %v", err)
} else if err = createUpdateHook(repo.WikiPath()); err != nil {
return fmt.Errorf("createUpdateHook: %v", err)
} else if err = createDelegateHooks(repo.WikiPath()); err != nil {
return fmt.Errorf("createDelegateHooks: %v", err)
}
return nil
}
Expand Down
4 changes: 2 additions & 2 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1003,8 +1003,8 @@ dashboard.git_gc_repos = Do garbage collection on repositories
dashboard.git_gc_repos_success = All repositories have done garbage collection successfully.
dashboard.resync_all_sshkeys = Rewrite '.ssh/authorized_keys' file (caution: non-Gitea keys will be lost)
dashboard.resync_all_sshkeys_success = All public keys have been rewritten successfully.
dashboard.resync_all_update_hooks = Rewrite all update hook of repositories (needed when custom config path is changed)
dashboard.resync_all_update_hooks_success = All repositories' update hook have been rewritten successfully.
dashboard.resync_all_hooks = Resync pre-receive, update and post-receive hooks of all repositories.
dashboard.resync_all_hooks_success = All repositories' pre-receive, update and post-receive hooks have been resynced successfully.
dashboard.reinit_missing_repos = Reinitialize all repository records that lost Git files
dashboard.reinit_missing_repos_success = All repository records that lost Git files have been reinitialized successfully.

Expand Down
4 changes: 2 additions & 2 deletions routers/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ func Dashboard(ctx *context.Context) {
success = ctx.Tr("admin.dashboard.resync_all_sshkeys_success")
err = models.RewriteAllPublicKeys()
case syncRepositoryUpdateHook:
success = ctx.Tr("admin.dashboard.resync_all_update_hooks_success")
err = models.RewriteRepositoryUpdateHook()
success = ctx.Tr("admin.dashboard.resync_all_hooks_success")
err = models.SyncRepositoryHooks()
case reinitMissingRepository:
success = ctx.Tr("admin.dashboard.reinit_missing_repos_success")
err = models.ReinitMissingRepositories()
Expand Down
Loading