internal/cli/gitaly/subcmd_hooks.go (137 lines of code) (raw):
package gitaly
import (
"context"
"errors"
"fmt"
"io"
"time"
"github.com/urfave/cli/v3"
gitalyauth "gitlab.com/gitlab-org/gitaly/v16/auth"
"gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/config"
"gitlab.com/gitlab-org/gitaly/v16/internal/grpc/client"
"gitlab.com/gitlab-org/gitaly/v16/internal/log"
"gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb"
"gitlab.com/gitlab-org/gitaly/v16/streamio"
"google.golang.org/grpc"
)
const (
flagStorage = "storage"
flagRepository = "repository"
flagConfig = "config"
)
func newHooksCommand() *cli.Command {
return &cli.Command{
Name: "hooks",
Usage: "manage Git hooks",
Description: "Manage hooks for a Git repository.",
Commands: []*cli.Command{
{
Name: "set",
Usage: "set custom hooks for a Git repository",
UsageText: `gitaly hooks set --storage <storage_name> --repository <path_on_storage> --config <gitaly_config_file> < <hooks_tarbar_file>.tar
Example: gitaly hooks set --storage default --repository @hashed/path/repository.git --config gitaly.config.toml < hooks_tarball.tar`,
Description: `Reads a tarball containing custom Git hooks from stdin and writes the hooks to the specified repository.
To remove custom Git hooks for a specified repository, run the set subcommand with an empty tarball file.`,
Action: setHooksAction,
Flags: []cli.Flag{
&cli.StringFlag{
Name: flagStorage,
Usage: "storage containing the repository",
},
&cli.StringFlag{
Name: flagRepository,
Usage: "repository to set hooks for",
Required: true,
},
gitalyConfigFlag(),
},
},
},
}
}
func setHooksAction(ctx context.Context, cmd *cli.Command) error {
log.ConfigureCommand()
cfg, err := loadConfig(cmd.String(flagConfig))
if err != nil {
return fmt.Errorf("load config: %w", err)
}
storage := cmd.String(flagStorage)
if storage == "" {
if len(cfg.Storages) != 1 {
return fmt.Errorf("multiple storages configured: use --storage to target storage explicitly")
}
storage = cfg.Storages[0].Name
}
address, err := getAddressWithScheme(cfg)
if err != nil {
return fmt.Errorf("get Gitaly address: %w", err)
}
conn, err := dial(ctx, address, cfg.Auth.Token, 10*time.Second)
if err != nil {
return fmt.Errorf("create connection: %w", err)
}
defer conn.Close()
if err := setRepoHooks(ctx, conn,
cmd.Reader,
storage,
cmd.String(flagRepository),
); err != nil {
return err
}
return nil
}
// setRepoHooks sets custom hooks for the specified repository. The specified reader is expected to
// provide a tarball containing custom git hooks within a `custom_hooks` directory.
func setRepoHooks(ctx context.Context, conn *grpc.ClientConn, reader io.Reader, storage, relativePath string) error {
repoClient := gitalypb.NewRepositoryServiceClient(conn)
stream, err := repoClient.SetCustomHooks(ctx)
if err != nil {
return fmt.Errorf("create repository client: %w", err)
}
// Send first request containing only repository information.
if err := stream.Send(&gitalypb.SetCustomHooksRequest{
Repository: &gitalypb.Repository{
StorageName: storage,
RelativePath: relativePath,
},
}); err != nil {
return err
}
// Configure streamWriter to transmit tarball data to stream.
streamWriter := streamio.NewWriter(func(p []byte) error {
return stream.Send(&gitalypb.SetCustomHooksRequest{Data: p})
})
if _, err := io.Copy(streamWriter, reader); err != nil {
// Ignore EOF errors to avoid race caused by server closing stream
// prematurely. This allows us to get an accurate error message as to
// why the stream was closed.
if !errors.Is(err, io.EOF) {
return fmt.Errorf("copying hooks archive: %w", err)
}
}
if _, err := stream.CloseAndRecv(); err != nil {
return fmt.Errorf("closing hooks archive stream: %w", err)
}
return nil
}
func dial(ctx context.Context, addr, token string, timeout time.Duration, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
opts = append(opts,
client.UnaryInterceptor(),
client.StreamInterceptor(),
)
if len(token) > 0 {
opts = append(opts,
grpc.WithPerRPCCredentials(
gitalyauth.RPCCredentialsV2(token),
),
)
}
return client.New(ctx, addr, client.WithGrpcOptions(opts))
}
func getAddressWithScheme(cfg config.Cfg) (string, error) {
switch {
case cfg.SocketPath != "":
return "unix:" + cfg.SocketPath, nil
case cfg.ListenAddr != "":
return "tcp://" + cfg.ListenAddr, nil
case cfg.TLSListenAddr != "":
return "tls://" + cfg.TLSListenAddr, nil
default:
return "", errors.New("no address configured")
}
}