diff --git a/cmd/soft/internal.go b/cmd/soft/internal.go new file mode 100644 index 0000000000000000000000000000000000000000..d742e3055c38ccbdae7135383815e7e28416368a --- /dev/null +++ b/cmd/soft/internal.go @@ -0,0 +1,187 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/charmbracelet/soft-serve/config" + "github.com/spf13/cobra" + gossh "golang.org/x/crypto/ssh" +) + +var ( + internalCmd = &cobra.Command{ + Use: "internal", + Short: "Internal Soft Serve API", + Long: `Soft Serve internal API. +This command is used to communicate with the Soft Serve SSH server.`, + Hidden: true, + } + + hookCmd = &cobra.Command{ + Use: "hook", + Short: "Run git server hooks", + Long: "Handles git server hooks. This includes pre-receive, update, and post-receive.", + } + + preReceiveCmd = &cobra.Command{ + Use: "pre-receive", + Short: "Run git pre-receive hook", + RunE: func(cmd *cobra.Command, args []string) error { + c, s, err := commonInit() + if err != nil { + return err + } + defer c.Close() //nolint:errcheck + defer s.Close() //nolint:errcheck + in, err := s.StdinPipe() + if err != nil { + return err + } + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + in.Write([]byte(scanner.Text())) + in.Write([]byte("\n")) + } + in.Close() //nolint:errcheck + b, err := s.Output("internal hook pre-receive") + if err != nil { + return err + } + cmd.Print(string(b)) + return nil + }, + } + + updateCmd = &cobra.Command{ + Use: "update", + Short: "Run git update hook", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + refName := args[0] + oldSha := args[1] + newSha := args[2] + c, s, err := commonInit() + if err != nil { + return err + } + defer c.Close() //nolint:errcheck + defer s.Close() //nolint:errcheck + b, err := s.Output(fmt.Sprintf("internal hook update %s %s %s", refName, oldSha, newSha)) + if err != nil { + return err + } + cmd.Print(string(b)) + return nil + }, + } + + postReceiveCmd = &cobra.Command{ + Use: "post-receive", + Short: "Run git post-receive hook", + RunE: func(cmd *cobra.Command, args []string) error { + c, s, err := commonInit() + if err != nil { + return err + } + defer c.Close() //nolint:errcheck + defer s.Close() //nolint:errcheck + in, err := s.StdinPipe() + if err != nil { + return err + } + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + in.Write([]byte(scanner.Text())) + in.Write([]byte("\n")) + } + in.Close() //nolint:errcheck + b, err := s.Output("internal hook post-receive") + if err != nil { + return err + } + cmd.Print(string(b)) + return nil + }, + } +) + +func init() { + hookCmd.AddCommand( + preReceiveCmd, + updateCmd, + postReceiveCmd, + ) + internalCmd.AddCommand( + hookCmd, + ) +} + +func commonInit() (c *gossh.Client, s *gossh.Session, err error) { + cfg := config.DefaultConfig() + // Git runs the hook within the repository's directory. + // Get the working directory to determine the repository name. + wd, err := os.Getwd() + if err != nil { + return + } + if !strings.HasPrefix(wd, cfg.RepoPath) { + err = fmt.Errorf("hook must be run from within repository directory") + return + } + repoName := strings.TrimPrefix(wd, cfg.RepoPath) + repoName = strings.TrimPrefix(repoName, fmt.Sprintf("%c", os.PathSeparator)) + c, err = newClient(cfg) + if err != nil { + return + } + s, err = newSession(c) + if err != nil { + return + } + s.Setenv("SOFT_SERVE_REPO_NAME", repoName) + return +} + +func newClient(cfg *config.Config) (*gossh.Client, error) { + // Only accept the server's host key. + pubKey, err := os.ReadFile(cfg.KeyPath) + if err != nil { + return nil, err + } + hostKey, err := gossh.ParsePrivateKey(pubKey) + if err != nil { + return nil, err + } + pemKey, err := os.ReadFile(cfg.InternalKeyPath) + if err != nil { + return nil, err + } + k, err := gossh.ParsePrivateKey(pemKey) + if err != nil { + return nil, err + } + cc := &gossh.ClientConfig{ + User: "internal", + Auth: []gossh.AuthMethod{ + gossh.PublicKeys(k), + }, + HostKeyCallback: gossh.FixedHostKey(hostKey.PublicKey()), + } + addr := fmt.Sprintf("%s:%d", cfg.BindAddr, cfg.Port) + c, err := gossh.Dial("tcp", addr, cc) + if err != nil { + return nil, err + } + return c, nil +} + +func newSession(c *gossh.Client) (*gossh.Session, error) { + s, err := c.NewSession() + if err != nil { + return nil, err + } + return s, nil +} diff --git a/cmd/soft/root.go b/cmd/soft/root.go index 6a536f228ad45f575beed27782f3bb4df83cbf20..5c8fa5980af58724493f52453a75a205a6382f2c 100644 --- a/cmd/soft/root.go +++ b/cmd/soft/root.go @@ -30,6 +30,7 @@ func init() { rootCmd.AddCommand( serveCmd, manCmd, + internalCmd, ) rootCmd.CompletionOptions.HiddenDefaultCmd = true diff --git a/cmd/soft/serve.go b/cmd/soft/serve.go index ecdd628744766a522b901747035c2913533858fc..6e79bb1e921e3bbc4d6a364873c77ec03da2153b 100644 --- a/cmd/soft/serve.go +++ b/cmd/soft/serve.go @@ -1,18 +1,28 @@ package main import ( + "bytes" "context" + "fmt" "log" "os" "os/signal" + "strings" "syscall" + "text/template" "time" + "github.com/charmbracelet/keygen" "github.com/charmbracelet/soft-serve/config" "github.com/charmbracelet/soft-serve/server" "github.com/spf13/cobra" ) +var ( + hookTmpl *template.Template + initHooks bool +) + var ( serveCmd = &cobra.Command{ Use: "serve", @@ -21,6 +31,48 @@ var ( Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { cfg := config.DefaultConfig() + // Internal API keypair + _, err := keygen.NewWithWrite( + strings.TrimSuffix(cfg.InternalKeyPath, "_ed25519"), + nil, + keygen.Ed25519, + ) + if err != nil { + return err + } + // Create git server hooks + if initHooks { + ex, err := os.Executable() + if err != nil { + return err + } + repos, err := os.ReadDir(cfg.RepoPath) + if err != nil { + return err + } + for _, repo := range repos { + for _, hook := range []string{"pre-receive", "update", "post-receive"} { + var data bytes.Buffer + var args string + hp := fmt.Sprintf("%s/%s/hooks/%s", cfg.RepoPath, repo.Name(), hook) + if hook == "update" { + args = "$1 $2 $3" + } + err = hookTmpl.Execute(&data, hookScript{ + Executable: ex, + Hook: hook, + Args: args, + }) + if err != nil { + return err + } + err = os.WriteFile(hp, data.Bytes(), 0755) //nolint:gosec + if err != nil { + return err + } + } + } + } s := server.NewServer(cfg) done := make(chan os.Signal, 1) @@ -42,3 +94,18 @@ var ( }, } ) + +type hookScript struct { + Executable string + Hook string + Args string +} + +func init() { + hookTmpl = template.New("hook") + hookTmpl, _ = hookTmpl.Parse(`#!/usr/bin/env bash +# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY +{{ .Executable }} internal hook {{ .Hook }} {{ .Args }} +`) + serveCmd.Flags().BoolVarP(&initHooks, "init-hooks", "i", false, "Initialize git hooks") +} diff --git a/config/config.go b/config/config.go index dc60e2dad1f2cd802a98d1ca3894c6db4bc0d8ba..57124f17ed47bfb1b3179b4a6224b41fcac1e9c2 100644 --- a/config/config.go +++ b/config/config.go @@ -7,6 +7,20 @@ import ( "github.com/caarlos0/env/v6" ) +// +type GitHookOption struct { + OldSha string + NewSha string + RefName string +} + +// GitHooks provides an interface for git server-side hooks. +type GitHooks interface { + PreReceive(string, []GitHookOption) + Update(string, GitHookOption) + PostReceive(string, []GitHookOption) +} + // Callbacks provides an interface that can be used to run callbacks on different events. type Callbacks interface { Tui(action string) @@ -20,6 +34,7 @@ type Config struct { Host string `env:"SOFT_SERVE_HOST" envDefault:"localhost"` Port int `env:"SOFT_SERVE_PORT" envDefault:"23231"` KeyPath string `env:"SOFT_SERVE_KEY_PATH"` + InternalKeyPath string `env:"SOFT_SERVE_INTERNAL_KEY_PATH"` RepoPath string `env:"SOFT_SERVE_REPO_PATH" envDefault:".repos"` InitialAdminKeys []string `env:"SOFT_SERVE_INITIAL_ADMIN_KEY" envSeparator:"\n"` Callbacks Callbacks @@ -37,6 +52,10 @@ func DefaultConfig() *Config { // NB: cross-platform-compatible path cfg.KeyPath = filepath.Join(".ssh", "soft_serve_server_ed25519") } + if cfg.InternalKeyPath == "" { + // NB: cross-platform-compatible path + cfg.InternalKeyPath = filepath.Join(".ssh", "soft_serve_internal_ed25519") + } return cfg.WithCallbacks(nil) } diff --git a/go.mod b/go.mod index ffc7aed12ac21ebb49549ca399fd16bc2f1e7789..3fdbaedeec2046914f8ee60d284c26f61980eac0 100755 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/charmbracelet/bubbles v0.11.0 github.com/charmbracelet/bubbletea v0.22.0 github.com/charmbracelet/glamour v0.4.0 - github.com/charmbracelet/lipgloss v0.5.0 + github.com/charmbracelet/lipgloss v0.5.1-0.20220615005615-2e17a8a06096 github.com/charmbracelet/wish v0.5.0 github.com/dustin/go-humanize v1.0.0 github.com/gliderlabs/ssh v0.3.4 @@ -16,7 +16,7 @@ require ( github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4 github.com/matryer/is v1.4.0 github.com/muesli/reflow v0.3.0 - github.com/muesli/termenv v0.12.0 + github.com/muesli/termenv v0.12.1-0.20220615005108-4e9068de9898 github.com/sergi/go-diff v1.1.0 golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 ) @@ -38,6 +38,7 @@ require ( github.com/acomagu/bufpipe v1.0.3 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52 v1.0.3 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/caarlos0/sshmarshal v0.1.0 // indirect github.com/containerd/console v1.0.3 // indirect diff --git a/go.sum b/go.sum index 33dfda0d298ab4e4aecf23c4bf0868352a12a29f..d9d968ca7481801e9f34810035af313f614eb5ea 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg= +github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/caarlos0/env/v6 v6.9.1 h1:zOkkjM0F6ltnQ5eBX6IPI41UP/KDGEK7rRPwGCNos8k= @@ -36,8 +38,9 @@ github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJ github.com/charmbracelet/keygen v0.3.0 h1:mXpsQcH7DDlST5TddmXNXjS0L7ECk4/kLQYyBcsan2Y= github.com/charmbracelet/keygen v0.3.0/go.mod h1:1ukgO8806O25lUZ5s0IrNur+RlwTBERlezdgW71F5rM= github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM= -github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= +github.com/charmbracelet/lipgloss v0.5.1-0.20220615005615-2e17a8a06096 h1:ai19sA3Zyg3DARevWCbdLOWt+MfWiE3e8voBqzFOgP8= +github.com/charmbracelet/lipgloss v0.5.1-0.20220615005615-2e17a8a06096/go.mod h1:D7uPgcyfB9T1Ug2mfJOnES17o47nz5oqIzSSVrpcviU= github.com/charmbracelet/wish v0.5.0 h1:FkkdNBFqrLABR1ciNrAL2KCxoyWfKhXnIGZw6GfAtPg= github.com/charmbracelet/wish v0.5.0/go.mod h1:5GAn5SrDSZ7cgKjnC+3kDmiIo7I6k4/AYiRzC4+tpCk= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= @@ -138,8 +141,8 @@ github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= -github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc= -github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A= +github.com/muesli/termenv v0.12.1-0.20220615005108-4e9068de9898 h1:0j+cbZdhLgpNxjg0nWCasHUA82fgWOXxxGgWNVOLS1I= +github.com/muesli/termenv v0.12.1-0.20220615005108-4e9068de9898/go.mod h1:bN6sPNtkiahdhHv2Xm6RGU16LSCxfbIZvMfqjOCfrR4= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= diff --git a/internal/config/config.go b/internal/config/config.go index 86cbc52b734dc1281c6ed0f6debc59f5523aa2cf..8f04f45b8315057943769854cff3a3b86cc99a72 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -170,6 +170,10 @@ func (cfg *Config) Reload() error { rm = md } r.SetReadme(rm, fp) + err := cfg.createHooks(r) + if err != nil { + return err + } } return nil } @@ -275,3 +279,87 @@ func templatize(mdt string, tmpl interface{}) (string, error) { } return buf.String(), nil } + +type hookScript struct { + Executable string + Hook string + Args string + Envs []string +} + +var hookTmpl *template.Template + +func (cfg *Config) createHooks(repo *git.Repo) error { + if hookTmpl == nil { + var err error + hookTmpl, err = template.New("hook").Parse(`#!/usr/bin/env bash +# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY +{{ range $_, $env := .Envs }} +{{ $env }} \{{ end }} +{{ .Executable }} internal hook {{ .Hook }} {{ .Args }} +`) + if err != nil { + return err + } + } + + err := ensureDir(filepath.Join(repo.Path(), "hooks")) + if err != nil { + return err + } + ex, err := os.Executable() + if err != nil { + return err + } + rp, err := filepath.Abs(cfg.Cfg.RepoPath) + if err != nil { + return err + } + kp, err := filepath.Abs(cfg.Cfg.KeyPath) + if err != nil { + return err + } + ikp, err := filepath.Abs(cfg.Cfg.InternalKeyPath) + if err != nil { + return err + } + envs := []string{ + fmt.Sprintf("SOFT_SERVE_BIND_ADDRESS=%s", cfg.Cfg.BindAddr), + fmt.Sprintf("SOFT_SERVE_PORT=%d", cfg.Cfg.Port), + fmt.Sprintf("SOFT_SERVE_HOST=%s", cfg.Cfg.Host), + fmt.Sprintf("SOFT_SERVE_REPO_PATH=%s", rp), + fmt.Sprintf("SOFT_SERVE_KEY_PATH=%s", kp), + fmt.Sprintf("SOFT_SERVE_INTERNAL_KEY_PATH=%s", ikp), + } + for _, hook := range []string{"pre-receive", "update", "post-receive"} { + var data bytes.Buffer + var args string + hp := filepath.Join(repo.Path(), "hooks", hook) + if hook == "update" { + args = "$1 $2 $3" + } + err = hookTmpl.Execute(&data, hookScript{ + Executable: ex, + Hook: hook, + Args: args, + Envs: envs, + }) + if err != nil { + return err + } + err = os.WriteFile(hp, data.Bytes(), 0755) //nolint:gosec + if err != nil { + return err + } + } + + return nil +} + +func ensureDir(path string) error { + _, err := os.Stat(path) + if os.IsNotExist(err) { + return os.MkdirAll(path, 0755) + } + return err +} diff --git a/internal/config/git.go b/internal/config/git.go index 54eb0df6bd291aebe75e16cb63fd86241d008187..4e5bbad9d06ae7991d64fa8b27a6cbeb069a5a17 100644 --- a/internal/config/git.go +++ b/internal/config/git.go @@ -4,10 +4,28 @@ import ( "log" "strings" + "github.com/charmbracelet/soft-serve/config" gm "github.com/charmbracelet/wish/git" "github.com/gliderlabs/ssh" ) +type HookOption = config.GitHookOption + +// PreReceive implements GitHooks interface. It is called before a git push is +// performed. +func (cfg *Config) PreReceive(repo string, args []HookOption) { +} + +// Update implements GitHooks interface. It is called during a git push once for +// each reference. +func (cfg *Config) Update(repo string, arg HookOption) { +} + +// PostReceive implements GitHooks interface. It is called after a git push is +// performed. +func (cfg *Config) PostReceive(repo string, args []HookOption) { +} + // Push registers Git push functionality for the given repo and key. func (cfg *Config) Push(repo string, pk ssh.PublicKey) { go func() { diff --git a/server/cmd/cmd.go b/server/cmd/cmd.go index 157a1c2bbbe82fd3f0d3b398e2050fe4c4281e20..e4b1d3610f8a1d53b2e17f8c46dd9c7383fe4765 100644 --- a/server/cmd/cmd.go +++ b/server/cmd/cmd.go @@ -57,6 +57,7 @@ func RootCommand() *cobra.Command { CatCommand(), ListCommand(), GitCommand(), + InternalCommand(), ) return rootCmd diff --git a/server/cmd/internal.go b/server/cmd/internal.go new file mode 100644 index 0000000000000000000000000000000000000000..bd6aab9e1ad2360dd3a141445f3fe03ac5455cec --- /dev/null +++ b/server/cmd/internal.go @@ -0,0 +1,154 @@ +package cmd + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/charmbracelet/soft-serve/internal/config" + "github.com/gliderlabs/ssh" + "github.com/spf13/cobra" + gossh "golang.org/x/crypto/ssh" +) + +// InternalCommand handles Soft Serve internal API requests. +func InternalCommand() *cobra.Command { + preReceiveCmd := &cobra.Command{ + Use: "pre-receive", + Short: "Run git pre-receive hook", + RunE: func(cmd *cobra.Command, args []string) error { + ac, s := fromContext(cmd) + repoName := getRepoName(s) + opts := make([]config.HookOption, 0) + scanner := bufio.NewScanner(s) + for scanner.Scan() { + fields := strings.Fields(scanner.Text()) + if len(fields) != 3 { + return fmt.Errorf("invalid pre-receive hook input: %s", scanner.Text()) + } + opts = append(opts, config.HookOption{ + OldSha: fields[0], + NewSha: fields[1], + RefName: fields[2], + }) + } + ac.PreReceive(repoName, opts) + return nil + }, + } + + updateCmd := &cobra.Command{ + Use: "update", + Short: "Run git update hook", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + ac, s := fromContext(cmd) + repoName := getRepoName(s) + ac.Update(repoName, config.HookOption{ + RefName: args[0], + OldSha: args[1], + NewSha: args[2], + }) + return nil + }, + } + + postReceiveCmd := &cobra.Command{ + Use: "post-receive", + Short: "Run git post-receive hook", + RunE: func(cmd *cobra.Command, args []string) error { + ac, s := fromContext(cmd) + repoName := getRepoName(s) + opts := make([]config.HookOption, 0) + scanner := bufio.NewScanner(s) + for scanner.Scan() { + fields := strings.Fields(scanner.Text()) + if len(fields) != 3 { + return fmt.Errorf("invalid post-receive hook input: %s", scanner.Text()) + } + opts = append(opts, config.HookOption{ + OldSha: fields[0], + NewSha: fields[1], + RefName: fields[2], + }) + } + ac.PostReceive(repoName, opts) + return nil + }, + } + + hookCmd := &cobra.Command{ + Use: "hook", + Short: "Run git server hooks", + } + + hookCmd.AddCommand( + preReceiveCmd, + updateCmd, + postReceiveCmd, + ) + + // Check if the session's public key matches the internal API key. + authorized := func(cmd *cobra.Command) (bool, error) { + ac, s := fromContext(cmd) + pk := s.PublicKey() + kp := ac.Cfg.InternalKeyPath + pemKey, err := os.ReadFile(kp) + if err != nil { + return false, err + } + priv, err := gossh.ParsePrivateKey(pemKey) + if err != nil { + return false, err + } + if !ssh.KeysEqual(pk, priv.PublicKey()) { + return false, ErrUnauthorized + } + return true, nil + } + internalCmd := &cobra.Command{ + Use: "internal", + Short: "Internal Soft Serve API", + Hidden: true, + SilenceUsage: true, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = false + authd, err := authorized(cmd) + if err != nil { + cmd.SilenceUsage = true + return err + } + if !authd { + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + authd, err := authorized(cmd) + if err != nil { + return err + } + if !authd { + return ErrUnauthorized + } + return cmd.Help() + }, + } + + internalCmd.AddCommand( + hookCmd, + ) + + return internalCmd +} + +func getRepoName(s ssh.Session) string { + var repoName string + for _, env := range s.Environ() { + if strings.HasPrefix(env, "SOFT_SERVE_REPO_NAME=") { + repoName = strings.TrimPrefix(env, "SOFT_SERVE_REPO_NAME=") + break + } + } + return repoName +}