Detailed changes
@@ -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
+}
@@ -30,6 +30,7 @@ func init() {
rootCmd.AddCommand(
serveCmd,
manCmd,
+ internalCmd,
)
rootCmd.CompletionOptions.HiddenDefaultCmd = true
@@ -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")
+}
@@ -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)
}
@@ -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
@@ -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=
@@ -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
+}
@@ -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() {
@@ -57,6 +57,7 @@ func RootCommand() *cobra.Command {
CatCommand(),
ListCommand(),
GitCommand(),
+ InternalCommand(),
)
return rootCmd
@@ -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
+}