diff --git a/changelogs/unreleased/jc-add-push-options.yml b/changelogs/unreleased/jc-add-push-options.yml new file mode 100644 index 0000000000000000000000000000000000000000..c830529ffc71af97171347917b7d9e86d6e88cdb --- /dev/null +++ b/changelogs/unreleased/jc-add-push-options.yml @@ -0,0 +1,5 @@ +--- +title: Add git push options +merge_request: 1756 +author: +type: changed diff --git a/changelogs/unreleased/jc-call-hook-rpc.yml b/changelogs/unreleased/jc-call-hook-rpc.yml new file mode 100644 index 0000000000000000000000000000000000000000..093e1d2c1c987eb23e2bbd18316df13cca130e79 --- /dev/null +++ b/changelogs/unreleased/jc-call-hook-rpc.yml @@ -0,0 +1,5 @@ +--- +title: Call Hook RPCs from gitaly-hooks binary +merge_request: 1740 +author: +type: performance diff --git a/client/receive_pack.go b/client/receive_pack.go index 0a92a83217b5d21481a380a22b3ecfc264cda03c..7047649fbf907b26a15c515a73c73586e21a6410 100644 --- a/client/receive_pack.go +++ b/client/receive_pack.go @@ -28,7 +28,7 @@ func ReceivePack(ctx context.Context, conn *grpc.ClientConn, stdin io.Reader, st return stream.Send(&gitalypb.SSHReceivePackRequest{Stdin: p}) }) - return streamHandler(func() (stdoutStderrResponse, error) { + return StreamHandler(func() (StdoutStderrResponse, error) { return stream.Recv() }, func(errC chan error) { _, errRecv := io.Copy(inWriter, stdin) diff --git a/client/std_stream.go b/client/std_stream.go index cf0d5e96e5c7524b33878aad925bf789ef01afdd..730153c3eddbc6e3ce8d9701807192c2cc09c45c 100644 --- a/client/std_stream.go +++ b/client/std_stream.go @@ -7,17 +7,22 @@ import ( "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb" ) -type stdoutStderrResponse interface { +// StdoutStderrResponse is an interface for RPC responses that need to stream stderr and stdout +type StdoutStderrResponse interface { GetExitStatus() *gitalypb.ExitStatus GetStderr() []byte GetStdout() []byte } -func streamHandler(recv func() (stdoutStderrResponse, error), send func(chan error), stdout, stderr io.Writer) (int32, error) { +// Sender is a function that sends input data to the stream +type Sender func(chan error) + +// StreamHandler takes care of sending and receiving to and from the stream +func StreamHandler(recv func() (StdoutStderrResponse, error), send Sender, stdout, stderr io.Writer) (int32, error) { var ( exitStatus int32 err error - resp stdoutStderrResponse + resp StdoutStderrResponse ) errC := make(chan error, 1) diff --git a/client/upload_archive.go b/client/upload_archive.go index 3dd5dc05c657a66c8e41daa91d7cf6642b020280..d87709cf2dd7c1776fa5315fe42099ccbd5e116f 100644 --- a/client/upload_archive.go +++ b/client/upload_archive.go @@ -28,7 +28,7 @@ func UploadArchive(ctx context.Context, conn *grpc.ClientConn, stdin io.Reader, return stream.Send(&gitalypb.SSHUploadArchiveRequest{Stdin: p}) }) - return streamHandler(func() (stdoutStderrResponse, error) { + return StreamHandler(func() (StdoutStderrResponse, error) { return stream.Recv() }, func(errC chan error) { _, errRecv := io.Copy(inWriter, stdin) diff --git a/client/upload_pack.go b/client/upload_pack.go index dcd48b6b16825d72b66225b042d722d16a5eae31..41aede96a1632306e770cbc0f782686e675d30a8 100644 --- a/client/upload_pack.go +++ b/client/upload_pack.go @@ -28,7 +28,7 @@ func UploadPack(ctx context.Context, conn *grpc.ClientConn, stdin io.Reader, std return stream.Send(&gitalypb.SSHUploadPackRequest{Stdin: p}) }) - return streamHandler(func() (stdoutStderrResponse, error) { + return StreamHandler(func() (StdoutStderrResponse, error) { return stream.Recv() }, func(errC chan error) { _, errRecv := io.Copy(inWriter, stdin) diff --git a/cmd/gitaly-hooks/hooks.go b/cmd/gitaly-hooks/hooks.go index c526c1033650ff473660f206af56689173f6bae4..51d304215486c39b219fb64ed08aa978fddaa2b1 100644 --- a/cmd/gitaly-hooks/hooks.go +++ b/cmd/gitaly-hooks/hooks.go @@ -4,14 +4,18 @@ import ( "context" "errors" "fmt" + "io" "net/http" "os" - "os/exec" - "path/filepath" + "strconv" "strings" - "gitlab.com/gitlab-org/gitaly/internal/command" + gitalyauth "gitlab.com/gitlab-org/gitaly/auth" + "gitlab.com/gitlab-org/gitaly/client" "gitlab.com/gitlab-org/gitaly/internal/log" + "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb" + "gitlab.com/gitlab-org/gitaly/streamio" + "google.golang.org/grpc" "gopkg.in/yaml.v2" ) @@ -36,17 +40,17 @@ func main() { os.Exit(0) } - gitlabRubyDir := os.Getenv("GITALY_RUBY_DIR") - if gitlabRubyDir == "" { - logger.Fatal(errors.New("GITALY_RUBY_DIR not set")) - } - - rubyHookPath := filepath.Join(gitlabRubyDir, "gitlab-shell", "hooks", subCmd) - ctx, cancel := context.WithCancel(context.Background()) defer cancel() - var hookCmd *exec.Cmd + conn, err := client.Dial(os.Getenv("GITALY_SOCKET"), dialOpts()) + if err != nil { + logger.Fatalf("error when dialing: %v", err) + } + + client := gitalypb.NewHookServiceClient(conn) + + var hookStatus int32 switch subCmd { case "update": @@ -54,23 +58,120 @@ func main() { if len(args) != 3 { logger.Fatal(errors.New("update hook missing required arguments")) } + ref, oldValue, newValue := args[0], args[1], args[2] + + req := &gitalypb.UpdateHookRequest{ + Repository: &gitalypb.Repository{ + StorageName: os.Getenv("GL_REPO_STORAGE"), + RelativePath: os.Getenv("GL_REPO_RELATIVE_PATH"), + GlRepository: os.Getenv("GL_REPOSITORY"), + }, + KeyId: os.Getenv("GL_ID"), + Ref: []byte(ref), + OldValue: oldValue, + NewValue: newValue, + } + + stream, err := client.UpdateHook(ctx, req) + if err != nil { + logger.Fatalf("error when starting command for %v: %v", subCmd, err) + } + + if hookStatus, err = sendAndRecv(stream, new(gitalypb.UpdateHookResponse), nil, os.Stdout, os.Stderr); err != nil { + logger.Fatalf("error when receiving data for %v: %v", subCmd, err) + } + case "pre-receive": + stream, err := client.PreReceiveHook(ctx) + if err != nil { + logger.Fatalf("error when getting stream client: %v", err) + } + + if err := stream.Send(&gitalypb.PreReceiveHookRequest{ + Repository: &gitalypb.Repository{ + StorageName: os.Getenv("GL_REPO_STORAGE"), + RelativePath: os.Getenv("GL_REPO_RELATIVE_PATH"), + GlRepository: os.Getenv("GL_REPOSITORY"), + }, + KeyId: os.Getenv("GL_ID"), + Protocol: os.Getenv("GL_PROTOCOL"), + }); err != nil { + logger.Fatalf("error when sending request: %v", err) + } + + f := sendFunc(streamio.NewWriter(func(p []byte) error { + return stream.Send(&gitalypb.PreReceiveHookRequest{Stdin: p}) + }), stream, os.Stdin) + + if hookStatus, err = sendAndRecv(stream, new(gitalypb.PreReceiveHookResponse), f, os.Stdout, os.Stderr); err != nil { + logger.Fatalf("error when receiving data for %v: %v", subCmd, err) + } + case "post-receive": + stream, err := client.PostReceiveHook(ctx) + if err != nil { + logger.Fatalf("error when getting stream client: %v", err) + } - hookCmd = exec.Command(rubyHookPath, args...) - case "pre-receive", "post-receive": - hookCmd = exec.Command(rubyHookPath) + if err := stream.Send(&gitalypb.PostReceiveHookRequest{ + Repository: &gitalypb.Repository{ + StorageName: os.Getenv("GL_REPO_STORAGE"), + RelativePath: os.Getenv("GL_REPO_RELATIVE_PATH"), + GlRepository: os.Getenv("GL_REPOSITORY"), + }, + KeyId: os.Getenv("GL_ID"), + GitPushOptions: gitPushOptions(), + }); err != nil { + logger.Fatalf("error when sending request: %v", err) + } + f := sendFunc(streamio.NewWriter(func(p []byte) error { + return stream.Send(&gitalypb.PostReceiveHookRequest{Stdin: p}) + }), stream, os.Stdin) + + if hookStatus, err = sendAndRecv(stream, new(gitalypb.PostReceiveHookResponse), f, os.Stdout, os.Stderr); err != nil { + logger.Fatalf("error when receiving data for %v: %v", subCmd, err) + } default: - logger.Fatal(errors.New("hook name invalid")) + logger.Fatal(errors.New("subcommand name invalid")) } - cmd, err := command.New(ctx, hookCmd, os.Stdin, os.Stdout, os.Stderr, os.Environ()...) + os.Exit(int(hookStatus)) +} + +func dialOpts() []grpc.DialOption { + return append(client.DefaultDialOpts, grpc.WithPerRPCCredentials(gitalyauth.RPCCredentials(os.Getenv("GITALY_TOKEN")))) +} + +func sendFunc(reqWriter io.Writer, stream grpc.ClientStream, stdin io.Reader) func(errC chan error) { + return func(errC chan error) { + _, errSend := io.Copy(reqWriter, stdin) + stream.CloseSend() + errC <- errSend + } +} + +func sendAndRecv(stream grpc.ClientStream, resp client.StdoutStderrResponse, sender client.Sender, stdout, stderr io.Writer) (int32, error) { + if sender == nil { + sender = func(err chan error) {} + } + return client.StreamHandler(func() (client.StdoutStderrResponse, error) { + err := stream.RecvMsg(resp) + return resp, err + }, sender, stdout, stderr) +} + +func gitPushOptions() []string { + gitPushOptionCount, err := strconv.Atoi(os.Getenv("GIT_PUSH_OPTION_COUNT")) if err != nil { - logger.Fatalf("error when starting command for %v: %v", rubyHookPath, err) + return []string{} } - if err = cmd.Wait(); err != nil { - os.Exit(1) + var gitPushOptions []string + + for i := 0; i < gitPushOptionCount; i++ { + gitPushOptions = append(gitPushOptions, os.Getenv(fmt.Sprintf("GIT_PUSH_OPTION_%d", i))) } + + return gitPushOptions } // GitlabShellConfig contains a subset of gitlabshell's config.yml diff --git a/cmd/gitaly-hooks/hooks_test.go b/cmd/gitaly-hooks/hooks_test.go index c7dbae128ffff9cb88c81bcac512cd97214b4cba..a448ef830e8d71ba703611672ca1dc3de25a0cc4 100644 --- a/cmd/gitaly-hooks/hooks_test.go +++ b/cmd/gitaly-hooks/hooks_test.go @@ -7,6 +7,7 @@ import ( "fmt" "io/ioutil" "log" + "net" "net/http" "net/http/httptest" "os" @@ -20,7 +21,10 @@ import ( "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitaly/internal/command" "gitlab.com/gitlab-org/gitaly/internal/config" + "gitlab.com/gitlab-org/gitaly/internal/git/hooks" + serverPkg "gitlab.com/gitlab-org/gitaly/internal/server" "gitlab.com/gitlab-org/gitaly/internal/testhelper" + "google.golang.org/grpc" "gopkg.in/yaml.v2" ) @@ -41,33 +45,55 @@ func TestHooksPrePostReceive(t *testing.T) { key := 1234 glRepository := "some_repo" + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + tempGitlabShellDir, cleanup := createTempGitlabShellDir(t) defer cleanup() changes := "abc" - ts := gitlabTestServer(t, "", "", secretToken, key, glRepository, changes, true) + gitlabShellDir := config.Config.GitlabShell.Dir + defer func() { + config.Config.GitlabShell.Dir = gitlabShellDir + }() + + config.Config.GitlabShell.Dir = tempGitlabShellDir + + gitPushOptions := []string{"gitpushoption1", "gitpushoption2"} + + ts := gitlabTestServer(t, "", "", secretToken, key, glRepository, changes, true, gitPushOptions...) defer ts.Close() writeTemporaryConfigFile(t, tempGitlabShellDir, GitlabShellConfig{GitlabURL: ts.URL}) + srv, socket := runFullServer(t) + defer srv.Stop() + writeShellSecretFile(t, tempGitlabShellDir, secretToken) for _, hook := range []string{"pre-receive", "post-receive"} { - for envName, env := range map[string][]string{"new": env(t, glRepository, tempGitlabShellDir, key), "old": oldEnv(t, glRepository, tempGitlabShellDir, key)} { - t.Run(hook+"."+envName, func(t *testing.T) { - var stderr, stdout bytes.Buffer - stdin := bytes.NewBuffer([]byte(changes)) - cmd := exec.Command(fmt.Sprintf("../../ruby/git-hooks/%s", hook)) - cmd.Stderr = &stderr - cmd.Stdout = &stdout - cmd.Stdin = stdin - cmd.Env = env - - require.NoError(t, cmd.Run()) - require.Empty(t, stderr.String()) - require.Empty(t, stdout.String()) - }) - } + t.Run(hook, func(t *testing.T) { + var stderr, stdout bytes.Buffer + stdin := bytes.NewBuffer([]byte(changes)) + cmd := exec.Command(fmt.Sprintf("../../ruby/git-hooks/%s", hook)) + cmd.Stderr = &stderr + cmd.Stdout = &stdout + cmd.Stdin = stdin + cmd.Env = env( + t, + glRepository, + tempGitlabShellDir, + testRepo.GetStorageName(), + testRepo.GetRelativePath(), + socket, + key, + gitPushOptions..., + ) + + require.NoError(t, cmd.Run()) + require.Empty(t, stderr.String()) + require.Empty(t, stdout.String()) + }) } } @@ -79,35 +105,47 @@ func TestHooksUpdate(t *testing.T) { defer cleanup() writeTemporaryConfigFile(t, tempGitlabShellDir, GitlabShellConfig{GitlabURL: "http://www.example.com"}) + testRepo, testRepoPath, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + os.Symlink(filepath.Join(config.Config.GitlabShell.Dir, "config.yml"), filepath.Join(tempGitlabShellDir, "config.yml")) + writeShellSecretFile(t, tempGitlabShellDir, "the wrong token") + gitlabShellDir := config.Config.GitlabShell.Dir + defer func() { + config.Config.GitlabShell.Dir = gitlabShellDir + }() + + config.Config.GitlabShell.Dir = tempGitlabShellDir + + srv, socket := runFullServer(t) + defer srv.Stop() + require.NoError(t, os.MkdirAll(filepath.Join(tempGitlabShellDir, "hooks", "update.d"), 0755)) testhelper.MustRunCommand(t, nil, "cp", "testdata/update", filepath.Join(tempGitlabShellDir, "hooks", "update.d", "update")) + tempFilePath := filepath.Join(testRepoPath, "tempfile") - for envName, env := range map[string][]string{"new": env(t, glRepository, tempGitlabShellDir, key), "old": oldEnv(t, glRepository, tempGitlabShellDir, key)} { - t.Run(envName, func(t *testing.T) { - refval, oldval, newval := "refval", "oldval", "newval" - var stdout, stderr bytes.Buffer + refval, oldval, newval := "refval", "oldval", "newval" + var stdout, stderr bytes.Buffer - cmd := exec.Command(fmt.Sprintf("../../ruby/git-hooks/%s", "update"), refval, oldval, newval) - cmd.Env = env - cmd.Stdout = &stdout - cmd.Stderr = &stderr + cmd := exec.Command("../../ruby/git-hooks/update", refval, oldval, newval) + cmd.Env = env(t, glRepository, tempGitlabShellDir, testRepo.GetStorageName(), testRepo.GetRelativePath(), socket, key) + cmd.Stdout = &stdout + cmd.Stderr = &stderr - require.NoError(t, cmd.Run()) - require.FileExists(t, "testdata/tempfile") - require.Empty(t, stdout.String()) - require.Empty(t, stderr.String()) + require.NoError(t, cmd.Run()) + require.Empty(t, stdout.String()) + require.Empty(t, stderr.String()) + require.FileExists(t, tempFilePath) - var inputs []string + var inputs []string - f, err := os.Open("testdata/tempfile") - require.NoError(t, err) - require.NoError(t, json.NewDecoder(f).Decode(&inputs)) - require.Equal(t, []string{refval, oldval, newval}, inputs) - require.NoError(t, os.Remove("testdata/tempfile")) - }) - } + f, err := os.Open(tempFilePath) + require.NoError(t, err) + require.NoError(t, json.NewDecoder(f).Decode(&inputs)) + require.Equal(t, []string{refval, oldval, newval}, inputs) + require.NoError(t, f.Close()) } func TestHooksPostReceiveFailed(t *testing.T) { @@ -118,6 +156,9 @@ func TestHooksPostReceiveFailed(t *testing.T) { tempGitlabShellDir, cleanup := createTempGitlabShellDir(t) defer cleanup() + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + // By setting the last parameter to false, the post-receive API call will // send back {"reference_counter_increased": false}, indicating something went wrong // with the call @@ -128,24 +169,30 @@ func TestHooksPostReceiveFailed(t *testing.T) { writeTemporaryConfigFile(t, tempGitlabShellDir, GitlabShellConfig{GitlabURL: ts.URL}) writeShellSecretFile(t, tempGitlabShellDir, secretToken) - for envName, env := range map[string][]string{"new": env(t, glRepository, tempGitlabShellDir, key), "old": oldEnv(t, glRepository, tempGitlabShellDir, key)} { - t.Run(envName, func(t *testing.T) { - var stdout, stderr bytes.Buffer + gitlabShellDir := config.Config.GitlabShell.Dir + defer func() { + config.Config.GitlabShell.Dir = gitlabShellDir + }() + + config.Config.GitlabShell.Dir = tempGitlabShellDir - cmd := exec.Command(fmt.Sprintf("../../ruby/git-hooks/%s", "post-receive")) - cmd.Env = env - cmd.Stdout = &stdout - cmd.Stderr = &stderr + srv, socket := runFullServer(t) + defer srv.Stop() - err := cmd.Run() - code, ok := command.ExitStatus(err) + var stdout, stderr bytes.Buffer - require.True(t, ok, "expect exit status in %v", err) - require.Equal(t, 1, code, "exit status") - require.Empty(t, stdout.String()) - require.Empty(t, stderr.String()) - }) - } + cmd := exec.Command(fmt.Sprintf("../../ruby/git-hooks/%s", "post-receive")) + cmd.Env = env(t, glRepository, tempGitlabShellDir, testRepo.GetStorageName(), testRepo.GetRelativePath(), socket, key) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + code, ok := command.ExitStatus(err) + + require.True(t, ok, "expect exit status in %v", err) + require.Equal(t, 1, code, "exit status") + require.Empty(t, stdout.String()) + require.Empty(t, stderr.String()) } func TestHooksNotAllowed(t *testing.T) { @@ -157,25 +204,33 @@ func TestHooksNotAllowed(t *testing.T) { defer cleanup() ts := gitlabTestServer(t, "", "", secretToken, key, glRepository, "", true) + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + defer ts.Close() writeTemporaryConfigFile(t, tempGitlabShellDir, GitlabShellConfig{GitlabURL: ts.URL}) writeShellSecretFile(t, tempGitlabShellDir, "the wrong token") - for envName, env := range map[string][]string{"new": env(t, glRepository, tempGitlabShellDir, key), "old": oldEnv(t, glRepository, tempGitlabShellDir, key)} { - t.Run(envName, func(t *testing.T) { - var stderr, stdout bytes.Buffer + gitlabShellDir := config.Config.GitlabShell.Dir + defer func() { + config.Config.GitlabShell.Dir = gitlabShellDir + }() - cmd := exec.Command(fmt.Sprintf("../../ruby/git-hooks/%s", "pre-receive")) - cmd.Stderr = &stderr - cmd.Stdout = &stdout - cmd.Env = env + config.Config.GitlabShell.Dir = tempGitlabShellDir + srv, socket := runFullServer(t) + defer srv.Stop() - require.Error(t, cmd.Run()) - require.Equal(t, "GitLab: 401 Unauthorized\n", stderr.String()) - require.Equal(t, "", stdout.String()) - }) - } + var stderr, stdout bytes.Buffer + + cmd := exec.Command(fmt.Sprintf("../../ruby/git-hooks/%s", "pre-receive")) + cmd.Stderr = &stderr + cmd.Stdout = &stdout + cmd.Env = env(t, glRepository, tempGitlabShellDir, testRepo.GetStorageName(), testRepo.GetRelativePath(), socket, key) + + require.Error(t, cmd.Run()) + require.Equal(t, "GitLab: 401 Unauthorized\n", stderr.String()) + require.Equal(t, "", stdout.String()) } func TestCheckOK(t *testing.T) { @@ -228,6 +283,20 @@ func TestCheckBadCreds(t *testing.T) { require.Empty(t, stdout.String()) } +func runFullServer(t *testing.T) (*grpc.Server, string) { + server := serverPkg.NewInsecure(nil) + serverSocketPath := testhelper.GetTemporaryGitalySocketFileName() + + listener, err := net.Listen("unix", serverSocketPath) + if err != nil { + t.Fatal(err) + } + + go server.Serve(listener) + + return server, "unix://" + serverSocketPath +} + func handleAllowed(t *testing.T, secretToken string, key int, glRepository, changes string) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { require.NoError(t, r.ParseForm()) @@ -262,7 +331,7 @@ func handlePreReceive(t *testing.T, secretToken, glRepository string) func(w htt } } -func handlePostReceive(t *testing.T, secretToken string, key int, glRepository, changes string, counterDecreased bool) func(w http.ResponseWriter, r *http.Request) { +func handlePostReceive(t *testing.T, secretToken string, key int, glRepository, changes string, counterDecreased bool, gitPushOptions ...string) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { require.NoError(t, r.ParseForm()) require.Equal(t, http.MethodPost, r.Method) @@ -272,6 +341,10 @@ func handlePostReceive(t *testing.T, secretToken string, key int, glRepository, require.Equal(t, fmt.Sprintf("key-%d", key), r.Form.Get("identifier")) require.Equal(t, changes, r.Form.Get("changes")) + if len(gitPushOptions) > 0 { + require.Equal(t, gitPushOptions, r.Form["push_options[]"]) + } + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(fmt.Sprintf(`{"reference_counter_decreased": %v}`, counterDecreased))) @@ -304,11 +377,12 @@ func gitlabTestServer(t *testing.T, key int, glRepository, changes string, - postReceiveCounterDecreased bool) *httptest.Server { + postReceiveCounterDecreased bool, + gitPushOptions ...string) *httptest.Server { mux := http.NewServeMux() mux.Handle("/api/v4/internal/allowed", http.HandlerFunc(handleAllowed(t, secretToken, key, glRepository, changes))) mux.Handle("/api/v4/internal/pre_receive", http.HandlerFunc(handlePreReceive(t, secretToken, glRepository))) - mux.Handle("/api/v4/internal/post_receive", http.HandlerFunc(handlePostReceive(t, secretToken, key, glRepository, changes, postReceiveCounterDecreased))) + mux.Handle("/api/v4/internal/post_receive", http.HandlerFunc(handlePostReceive(t, secretToken, key, glRepository, changes, postReceiveCounterDecreased, gitPushOptions...))) mux.Handle("/api/v4/internal/check", http.HandlerFunc(handleCheck(t, user, password))) return httptest.NewServer(mux) @@ -332,14 +406,13 @@ func writeTemporaryConfigFile(t *testing.T, dir string, config GitlabShellConfig return path } -func env(t *testing.T, glRepo, gitlabShellDir string, key int) []string { - rubyDir, err := filepath.Abs("../../ruby") - require.NoError(t, err) - - return append(oldEnv(t, glRepo, gitlabShellDir, key), []string{ +func env(t *testing.T, glRepo, gitlabShellDir, glStorage, glRelativePath, gitalySocket string, key int, gitPushOptions ...string) []string { + return append(append(oldEnv(t, glRepo, gitlabShellDir, key), []string{ "GITALY_BIN_DIR=testdata/gitaly-libexec", - fmt.Sprintf("GITALY_RUBY_DIR=%s", rubyDir), - }...) + fmt.Sprintf("GL_REPO_STORAGE=%s", glStorage), + fmt.Sprintf("GL_REPO_RELATIVE_PATH=%s", glRelativePath), + fmt.Sprintf("GITALY_SOCKET=%s", gitalySocket), + }...), hooks.GitPushOptions(gitPushOptions)...) } func oldEnv(t *testing.T, glRepo, gitlabShellDir string, key int) []string { @@ -351,7 +424,6 @@ func oldEnv(t *testing.T, glRepo, gitlabShellDir string, key int) []string { fmt.Sprintf("GITALY_LOG_DIR=%s", gitlabShellDir), "GITALY_LOG_LEVEL=info", "GITALY_LOG_FORMAT=json", - fmt.Sprintf("GITALY_LOG_DIR=%s", gitlabShellDir), }, os.Environ()...) } diff --git a/cmd/gitaly-hooks/testdata/update b/cmd/gitaly-hooks/testdata/update index a4076ec24c08532232941bc3bcc649105e1890d9..e982c4d17e02abc8b9ee04548873f966100cb058 100755 --- a/cmd/gitaly-hooks/testdata/update +++ b/cmd/gitaly-hooks/testdata/update @@ -1,4 +1,4 @@ #!/usr/bin/env ruby require 'json' -open('testdata/tempfile', 'w') { |f| f.puts(JSON.dump(ARGV)) } +open('tempfile', 'w') { |f| f.puts(JSON.dump(ARGV)) } diff --git a/internal/git/hooks/hooks.go b/internal/git/hooks/hooks.go index c77c3ed1e373ba8f7f080901f2b0d7492c1b9f2e..0bc02103a77c8cfe8f4f0729cc2c79cdcabd8d37 100644 --- a/internal/git/hooks/hooks.go +++ b/internal/git/hooks/hooks.go @@ -1,6 +1,7 @@ package hooks import ( + "fmt" "os" "path" @@ -25,3 +26,15 @@ func Path() string { return path.Join(config.Config.Ruby.Dir, "git-hooks") } + +// GitPushOptions turns a slice of git push option values into a GIT_PUSH_OPTION_COUNT and individual +// GIT_PUSH_OPTION_0, GIT_PUSH_OPTION_1 etc. +func GitPushOptions(options []string) []string { + envVars := []string{fmt.Sprintf("GIT_PUSH_OPTION_COUNT=%d", len(options))} + + for i, pushOption := range options { + envVars = append(envVars, fmt.Sprintf("GIT_PUSH_OPTION_%d=%s", i, pushOption)) + } + + return envVars +} diff --git a/internal/service/hooks/post_receive.go b/internal/service/hooks/post_receive.go index 72631737051e7501162778e29a8ae8c6888a12d4..55ec0323f87615e49ef6fb9fa720183d57c34389 100644 --- a/internal/service/hooks/post_receive.go +++ b/internal/service/hooks/post_receive.go @@ -4,6 +4,7 @@ import ( "errors" "os/exec" + "gitlab.com/gitlab-org/gitaly/internal/git/hooks" "gitlab.com/gitlab-org/gitaly/internal/helper" "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb" "gitlab.com/gitlab-org/gitaly/streamio" @@ -24,6 +25,8 @@ func (s *server) PostReceiveHook(stream gitalypb.HookService_PostReceiveHookServ return helper.ErrInternal(err) } + hookEnv = append(hookEnv, hooks.GitPushOptions(firstRequest.GetGitPushOptions())...) + stdin := streamio.NewReader(func() ([]byte, error) { req, err := stream.Recv() return req.GetStdin(), err diff --git a/internal/service/hooks/post_receive_test.go b/internal/service/hooks/post_receive_test.go index cb5bd1c581c228552e9c8a232f910dbf5d178bc7..1dfee6948d90399e081fae9eb1b91b1612127864 100644 --- a/internal/service/hooks/post_receive_test.go +++ b/internal/service/hooks/post_receive_test.go @@ -65,7 +65,7 @@ func TestPostReceive(t *testing.T) { { desc: "valid stdin", stdin: bytes.NewBufferString("a\nb\nc\nd\ne\nf\ng"), - req: gitalypb.PostReceiveHookRequest{Repository: testRepo, KeyId: "key_id"}, + req: gitalypb.PostReceiveHookRequest{Repository: testRepo, KeyId: "key_id", GitPushOptions: []string{"option0", "option1"}}, status: 0, stdout: "OK", stderr: "", @@ -73,7 +73,7 @@ func TestPostReceive(t *testing.T) { { desc: "missing stdin", stdin: bytes.NewBuffer(nil), - req: gitalypb.PostReceiveHookRequest{Repository: testRepo, KeyId: "key_id"}, + req: gitalypb.PostReceiveHookRequest{Repository: testRepo, KeyId: "key_id", GitPushOptions: []string{"option0"}}, status: 1, stdout: "", stderr: "FAIL", @@ -81,7 +81,15 @@ func TestPostReceive(t *testing.T) { { desc: "missing key_id", stdin: bytes.NewBuffer(nil), - req: gitalypb.PostReceiveHookRequest{Repository: testRepo}, + req: gitalypb.PostReceiveHookRequest{Repository: testRepo, GitPushOptions: []string{"option0"}}, + status: 1, + stdout: "", + stderr: "FAIL", + }, + { + desc: "missing git push option", + stdin: bytes.NewBuffer(nil), + req: gitalypb.PostReceiveHookRequest{Repository: testRepo, KeyId: "key_id"}, status: 1, stdout: "", stderr: "FAIL", diff --git a/internal/service/hooks/testdata/gitlab-shell/hooks/post-receive b/internal/service/hooks/testdata/gitlab-shell/hooks/post-receive index 6f0207819b90d2adcf5190dcd3d9602cc09df99f..aa0b008dea3c52471bb2f3f5f640059a87e4dbb8 100755 --- a/internal/service/hooks/testdata/gitlab-shell/hooks/post-receive +++ b/internal/service/hooks/testdata/gitlab-shell/hooks/post-receive @@ -4,5 +4,7 @@ abort("FAIL") if $stdin.read.empty? abort("FAIL") if %w[GL_ID GL_REPOSITORY].any? { |k| ENV[k].empty? } +# git push options are not required. This is only for the sake of testing the values get through +abort("FAIL") if %w[GIT_PUSH_OPTION_COUNT GIT_PUSH_OPTION_0].any? { |k| ENV[k].empty? } puts "OK" diff --git a/internal/service/hooks/update.go b/internal/service/hooks/update.go index d7c9c5475a43c3f1ac313f60388869e000493f68..211401113e686ba4ca1f5774dad40a91f4604e5f 100644 --- a/internal/service/hooks/update.go +++ b/internal/service/hooks/update.go @@ -28,6 +28,7 @@ func (s *server) UpdateHook(in *gitalypb.UpdateHookRequest, stream gitalypb.Hook } c := exec.Command(gitlabShellHook("update"), string(in.GetRef()), in.GetOldValue(), in.GetNewValue()) + c.Dir = repoPath status, err := streamCommandResponse( @@ -42,7 +43,7 @@ func (s *server) UpdateHook(in *gitalypb.UpdateHookRequest, stream gitalypb.Hook return helper.ErrInternal(err) } - if err := stream.SendMsg(&gitalypb.PreReceiveHookResponse{ + if err := stream.SendMsg(&gitalypb.UpdateHookResponse{ ExitStatus: &gitalypb.ExitStatus{Value: status}, }); err != nil { return helper.ErrInternal(err) diff --git a/proto/go/gitalypb/hook.pb.go b/proto/go/gitalypb/hook.pb.go index 5da3a44146bea62a557e773523fb4062233897ea..291aff7d24f94a25d625ffef079652dcf27e8cbd 100644 --- a/proto/go/gitalypb/hook.pb.go +++ b/proto/go/gitalypb/hook.pb.go @@ -146,6 +146,7 @@ type PostReceiveHookRequest struct { Repository *Repository `protobuf:"bytes,1,opt,name=repository,proto3" json:"repository,omitempty"` KeyId string `protobuf:"bytes,2,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty"` Stdin []byte `protobuf:"bytes,3,opt,name=stdin,proto3" json:"stdin,omitempty"` + GitPushOptions []string `protobuf:"bytes,4,rep,name=git_push_options,json=gitPushOptions,proto3" json:"git_push_options,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -197,6 +198,13 @@ func (m *PostReceiveHookRequest) GetStdin() []byte { return nil } +func (m *PostReceiveHookRequest) GetGitPushOptions() []string { + if m != nil { + return m.GitPushOptions + } + return nil +} + type PostReceiveHookResponse struct { Stdout []byte `protobuf:"bytes,1,opt,name=stdout,proto3" json:"stdout,omitempty"` Stderr []byte `protobuf:"bytes,2,opt,name=stderr,proto3" json:"stderr,omitempty"` @@ -390,35 +398,37 @@ func init() { func init() { proto.RegisterFile("hook.proto", fileDescriptor_3eef30da1c11ee1b) } var fileDescriptor_3eef30da1c11ee1b = []byte{ - // 434 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x94, 0xcf, 0x8e, 0xd3, 0x30, - 0x10, 0xc6, 0xe5, 0x96, 0x56, 0xed, 0xb4, 0xe2, 0x8f, 0xc5, 0x96, 0x12, 0x04, 0x54, 0x39, 0xe5, - 0x42, 0x5b, 0xba, 0x6f, 0x80, 0x84, 0x04, 0xb7, 0xca, 0x2b, 0x38, 0x70, 0xa0, 0x4a, 0xeb, 0xa1, - 0x6b, 0x35, 0x74, 0x82, 0xed, 0x76, 0x37, 0x07, 0x78, 0x0a, 0x24, 0xee, 0x1c, 0x79, 0x44, 0x4e, - 0x28, 0x76, 0x36, 0x1b, 0xb6, 0xd9, 0xe3, 0xf6, 0xe6, 0x99, 0x9f, 0x3d, 0xdf, 0x7c, 0xa3, 0x49, - 0x00, 0xce, 0x89, 0x36, 0xe3, 0x54, 0x93, 0x25, 0xde, 0x5e, 0x2b, 0x1b, 0x27, 0x59, 0xd0, 0x37, - 0xe7, 0xb1, 0x46, 0xe9, 0xb3, 0xe1, 0x4f, 0x06, 0x27, 0x73, 0x8d, 0x02, 0x57, 0xa8, 0xf6, 0xf8, - 0x8e, 0x68, 0x23, 0xf0, 0xdb, 0x0e, 0x8d, 0xe5, 0x33, 0x00, 0x8d, 0x29, 0x19, 0x65, 0x49, 0x67, - 0x43, 0x36, 0x62, 0x51, 0x6f, 0xc6, 0xc7, 0xbe, 0xc8, 0x58, 0x94, 0x44, 0x54, 0x6e, 0xf1, 0x13, - 0x68, 0x6f, 0x30, 0x5b, 0x28, 0x39, 0x6c, 0x8c, 0x58, 0xd4, 0x15, 0xad, 0x0d, 0x66, 0xef, 0x25, - 0x0f, 0xa0, 0xe3, 0xd4, 0x56, 0x94, 0x0c, 0x9b, 0x0e, 0x94, 0x31, 0x7f, 0x0c, 0x2d, 0x63, 0xa5, - 0xda, 0x0e, 0xef, 0x8d, 0x58, 0xd4, 0x17, 0x3e, 0x08, 0xbf, 0xc3, 0xe0, 0x66, 0x57, 0x26, 0xa5, - 0xad, 0x41, 0x3e, 0x80, 0xb6, 0xb1, 0x92, 0x76, 0xd6, 0xb5, 0xd4, 0x17, 0x45, 0x54, 0xe4, 0x51, - 0x6b, 0x27, 0xed, 0xf3, 0xa8, 0x35, 0x3f, 0x85, 0x1e, 0x5e, 0x2a, 0xbb, 0x30, 0x36, 0xb6, 0x3b, - 0xe3, 0xe4, 0x2b, 0x3e, 0xde, 0x5e, 0x2a, 0x7b, 0xe6, 0x88, 0x00, 0x2c, 0xcf, 0x61, 0x06, 0x83, - 0x39, 0x19, 0x7b, 0xb7, 0x53, 0x29, 0x9d, 0x37, 0xab, 0xce, 0x7f, 0xc0, 0x93, 0x03, 0xe9, 0x63, - 0x5a, 0xff, 0xc3, 0xe0, 0xd1, 0x87, 0x54, 0xc6, 0xf6, 0xae, 0x6c, 0x3f, 0x84, 0xa6, 0xc6, 0x2f, - 0x85, 0xe9, 0xfc, 0xc8, 0x9f, 0x41, 0x97, 0x12, 0xb9, 0xd8, 0xc7, 0xc9, 0x0e, 0xdd, 0x1a, 0x74, - 0x45, 0x87, 0x12, 0xf9, 0x31, 0x8f, 0x73, 0xb8, 0xc5, 0x8b, 0x02, 0xb6, 0x3c, 0xdc, 0xe2, 0x85, - 0x83, 0x61, 0x06, 0xbc, 0xda, 0xeb, 0x11, 0xe7, 0x34, 0xfb, 0xdd, 0x80, 0x5e, 0xae, 0x7a, 0x86, - 0x7a, 0xaf, 0x56, 0xc8, 0x3f, 0xc3, 0xfd, 0xff, 0x37, 0x96, 0x3f, 0xbf, 0xaa, 0x50, 0xfb, 0x7d, - 0x05, 0x2f, 0x6e, 0xc3, 0xde, 0x45, 0xd8, 0xfd, 0xfb, 0x2b, 0x6a, 0x75, 0x1a, 0x01, 0x7b, 0x1d, - 0xb1, 0x29, 0xe3, 0x31, 0x3c, 0xb8, 0xb1, 0x17, 0xfc, 0xba, 0x42, 0xed, 0xae, 0x06, 0x2f, 0x6f, - 0xe5, 0xf5, 0x12, 0x73, 0x80, 0xeb, 0x69, 0xf2, 0xa7, 0x57, 0xaf, 0x0f, 0xb6, 0x21, 0x08, 0xea, - 0xd0, 0x41, 0xcd, 0x29, 0x7b, 0x33, 0xfd, 0x94, 0xdf, 0x4c, 0xe2, 0xe5, 0x78, 0x45, 0x5f, 0x27, - 0xfe, 0xf8, 0x8a, 0xf4, 0x7a, 0xe2, 0xdf, 0x4f, 0xdc, 0x5f, 0x60, 0xb2, 0xa6, 0x22, 0x4e, 0x97, - 0xcb, 0xb6, 0x4b, 0x9d, 0xfe, 0x0b, 0x00, 0x00, 0xff, 0xff, 0x90, 0x69, 0xbd, 0x6b, 0xba, 0x04, - 0x00, 0x00, + // 467 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x94, 0x4f, 0x6f, 0xd3, 0x4c, + 0x10, 0xc6, 0xb5, 0x49, 0x13, 0xc5, 0x93, 0xa8, 0x6f, 0xdf, 0x15, 0x0d, 0xc6, 0x08, 0x88, 0x7c, + 0xf2, 0x85, 0x24, 0xa4, 0xdf, 0x00, 0x09, 0x09, 0x4e, 0x44, 0x5b, 0xc1, 0x81, 0x03, 0x96, 0x13, + 0x0f, 0xce, 0x2a, 0xc6, 0x63, 0x76, 0xd7, 0x69, 0x7d, 0x80, 0x4f, 0x81, 0xc4, 0x1d, 0x6e, 0x7c, + 0x44, 0x4e, 0xc8, 0x6b, 0x37, 0x0d, 0x4d, 0x7a, 0x6c, 0x6f, 0x3b, 0xf3, 0xdb, 0x9d, 0x3f, 0x8f, + 0x1e, 0x1b, 0x60, 0x45, 0xb4, 0x1e, 0xe7, 0x8a, 0x0c, 0xf1, 0x6e, 0x22, 0x4d, 0x94, 0x96, 0xde, + 0x40, 0xaf, 0x22, 0x85, 0x71, 0x9d, 0xf5, 0xbf, 0x33, 0x38, 0x9d, 0x2b, 0x14, 0xb8, 0x44, 0xb9, + 0xc1, 0xd7, 0x44, 0x6b, 0x81, 0x5f, 0x0a, 0xd4, 0x86, 0xcf, 0x00, 0x14, 0xe6, 0xa4, 0xa5, 0x21, + 0x55, 0xba, 0x6c, 0xc4, 0x82, 0xfe, 0x8c, 0x8f, 0xeb, 0x22, 0x63, 0xb1, 0x25, 0x62, 0xe7, 0x16, + 0x3f, 0x85, 0xee, 0x1a, 0xcb, 0x50, 0xc6, 0x6e, 0x6b, 0xc4, 0x02, 0x47, 0x74, 0xd6, 0x58, 0xbe, + 0x89, 0xb9, 0x07, 0x3d, 0xdb, 0x6d, 0x49, 0xa9, 0xdb, 0xb6, 0x60, 0x1b, 0xf3, 0x07, 0xd0, 0xd1, + 0x26, 0x96, 0x99, 0x7b, 0x34, 0x62, 0xc1, 0x40, 0xd4, 0x81, 0xff, 0x15, 0x86, 0x37, 0xa7, 0xd2, + 0x39, 0x65, 0x1a, 0xf9, 0x10, 0xba, 0xda, 0xc4, 0x54, 0x18, 0x3b, 0xd2, 0x40, 0x34, 0x51, 0x93, + 0x47, 0xa5, 0x6c, 0xeb, 0x3a, 0x8f, 0x4a, 0xf1, 0x33, 0xe8, 0xe3, 0xa5, 0x34, 0xa1, 0x36, 0x91, + 0x29, 0xb4, 0x6d, 0xbf, 0xb3, 0xc7, 0xab, 0x4b, 0x69, 0xce, 0x2d, 0x11, 0x80, 0xdb, 0xb3, 0xff, + 0x8b, 0xc1, 0x70, 0x4e, 0xda, 0xdc, 0xad, 0x2c, 0xdb, 0xd5, 0xdb, 0x3b, 0xab, 0xf3, 0x00, 0x4e, + 0x12, 0x69, 0xc2, 0xbc, 0xd0, 0xab, 0x90, 0x72, 0x23, 0x29, 0xd3, 0xee, 0xd1, 0xa8, 0x1d, 0x38, + 0xe2, 0x38, 0x91, 0x66, 0x5e, 0xe8, 0xd5, 0xdb, 0x3a, 0xeb, 0x7f, 0x83, 0x87, 0x7b, 0x43, 0xde, + 0xa7, 0x4a, 0xbf, 0x19, 0xfc, 0xff, 0x2e, 0x8f, 0x23, 0x73, 0x57, 0x02, 0x9d, 0x40, 0x5b, 0xe1, + 0xa7, 0x46, 0x9e, 0xea, 0xc8, 0x1f, 0x83, 0x43, 0x69, 0x1c, 0x6e, 0xa2, 0xb4, 0x40, 0xeb, 0x18, + 0x47, 0xf4, 0x28, 0x8d, 0xdf, 0x57, 0x71, 0x05, 0x33, 0xbc, 0x68, 0x60, 0xa7, 0x86, 0x19, 0x5e, + 0x58, 0xe8, 0x97, 0xc0, 0x77, 0x67, 0xbd, 0x47, 0x9d, 0x66, 0x3f, 0x5b, 0xd0, 0xaf, 0xba, 0x9e, + 0xa3, 0xda, 0xc8, 0x25, 0xf2, 0x8f, 0x70, 0xfc, 0xaf, 0xb9, 0xf9, 0x93, 0xab, 0x0a, 0x07, 0x3f, + 0x45, 0xef, 0xe9, 0x6d, 0xb8, 0xde, 0xc2, 0x77, 0xfe, 0xfc, 0x08, 0x3a, 0xbd, 0x96, 0xc7, 0x5e, + 0x04, 0x6c, 0xca, 0x78, 0x04, 0xff, 0xdd, 0xf0, 0x05, 0xbf, 0xae, 0x70, 0xd0, 0xd5, 0xde, 0xb3, + 0x5b, 0xf9, 0xe1, 0x16, 0x73, 0x80, 0x6b, 0x35, 0xf9, 0xa3, 0xab, 0xd7, 0x7b, 0x6e, 0xf0, 0xbc, + 0x43, 0x68, 0xaf, 0xe6, 0x94, 0xbd, 0x9c, 0x7e, 0xa8, 0x6e, 0xa6, 0xd1, 0x62, 0xbc, 0xa4, 0xcf, + 0x93, 0xfa, 0xf8, 0x9c, 0x54, 0x32, 0xa9, 0xdf, 0x4f, 0xec, 0x0f, 0x63, 0x92, 0x50, 0x13, 0xe7, + 0x8b, 0x45, 0xd7, 0xa6, 0xce, 0xfe, 0x06, 0x00, 0x00, 0xff, 0xff, 0x75, 0x25, 0x33, 0xf6, 0xe5, + 0x04, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. diff --git a/proto/hook.proto b/proto/hook.proto index 194036689cadee4328f37a6756ddcc26c76c913a..1c78842e7a42d2f412dd301ee9785efd5c8b7f4e 100644 --- a/proto/hook.proto +++ b/proto/hook.proto @@ -44,6 +44,7 @@ message PostReceiveHookRequest { Repository repository = 1; string key_id = 2; bytes stdin = 3; + repeated string git_push_options = 4; } message PostReceiveHookResponse{ diff --git a/ruby/git-hooks/gitlab-shell-hook b/ruby/git-hooks/gitlab-shell-hook index 550f31d83e1f8e91fede188d5414689c9ee40c51..f47c8697fa6209c55f5f5368d3520797b53add72 100755 --- a/ruby/git-hooks/gitlab-shell-hook +++ b/ruby/git-hooks/gitlab-shell-hook @@ -1,9 +1,4 @@ #!/bin/sh # This is the single source of truth for where Gitaly's embedded Git hooks are. -if [ -n "$GITALY_BIN_DIR" ]; then - exec "$GITALY_BIN_DIR/gitaly-hooks" "$(basename $0)" "$@" -else - hooks_dir="$(dirname $0)/../gitlab-shell/hooks" - exec "$hooks_dir/$(basename $0)" "$@" -fi +exec "$GITALY_BIN_DIR/gitaly-hooks" "$(basename $0)" "$@" diff --git a/ruby/proto/gitaly/hook_pb.rb b/ruby/proto/gitaly/hook_pb.rb index 3f0b91b8952dcdb24250ef9e26db9ec974e2c822..a20dddf5eeb7e58da3fcb1c0d8a072521fcdfe76 100644 --- a/ruby/proto/gitaly/hook_pb.rb +++ b/ruby/proto/gitaly/hook_pb.rb @@ -20,6 +20,7 @@ Google::Protobuf::DescriptorPool.generated_pool.build do optional :repository, :message, 1, "gitaly.Repository" optional :key_id, :string, 2 optional :stdin, :bytes, 3 + repeated :git_push_options, :string, 4 end add_message "gitaly.PostReceiveHookResponse" do optional :stdout, :bytes, 1