Skip to content

Commit

Permalink
feat(server): add git hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed May 2, 2023
1 parent b957470 commit 227f178
Show file tree
Hide file tree
Showing 24 changed files with 674 additions and 90 deletions.
215 changes: 215 additions & 0 deletions cmd/soft/hook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package main

import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/charmbracelet/keygen"
"github.com/charmbracelet/soft-serve/server/config"
"github.com/spf13/cobra"
gossh "golang.org/x/crypto/ssh"
)

var (
configPath string

hookCmd = &cobra.Command{
Use: "hook",
Short: "Run git server hooks",
Long: "Handles git server hooks. This includes pre-receive, update, and post-receive.",
Hidden: true,
}

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("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("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("hook post-receive")
if err != nil {
return err
}
cmd.Print(string(b))
return nil
},
}

postUpdateCmd = &cobra.Command{
Use: "post-update",
Short: "Run git post-update 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
b, err := s.Output(fmt.Sprintf("hook post-update %s", strings.Join(args, " ")))
if err != nil {
return err
}
cmd.Print(string(b))
return nil
},
}
)

func init() {
hookCmd.AddCommand(
preReceiveCmd,
updateCmd,
postReceiveCmd,
postUpdateCmd,
)

hookCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "path to config file")
}

func commonInit() (c *gossh.Client, s *gossh.Session, err error) {
cfg, err := config.ParseConfig(configPath)
if err != nil {
return
}

// Use absolute path.
cfg.DataPath = filepath.Dir(configPath)

// 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
}

rs, err := filepath.Abs(filepath.Join(cfg.DataPath, "repos"))
if err != nil {
return
}

if !strings.HasPrefix(wd, rs) {
err = fmt.Errorf("hook must be run from within repository directory")
return
}
repoName := strings.TrimPrefix(wd, rs)
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.
pk, err := keygen.New(filepath.Join(cfg.DataPath, cfg.SSH.KeyPath), nil, keygen.Ed25519)
if err != nil {
return nil, err
}
hostKey, err := gossh.ParsePrivateKey(pk.PrivateKeyPEM())
if err != nil {
return nil, err
}
ik, err := keygen.New(filepath.Join(cfg.DataPath, cfg.SSH.InternalKeyPath), nil, keygen.Ed25519)
if err != nil {
return nil, err
}
k, err := gossh.ParsePrivateKey(ik.PrivateKeyPEM())
if err != nil {
return nil, err
}
cc := &gossh.ClientConfig{
User: "internal",
Auth: []gossh.AuthMethod{
gossh.PublicKeys(k),
},
HostKeyCallback: gossh.FixedHostKey(hostKey.PublicKey()),
}
c, err := gossh.Dial("tcp", cfg.SSH.ListenAddr, 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
}
1 change: 1 addition & 0 deletions cmd/soft/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func init() {
rootCmd.AddCommand(
serveCmd,
manCmd,
hookCmd,
)
rootCmd.CompletionOptions.HiddenDefaultCmd = true

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ require (
goji.io v2.0.2+incompatible
golang.org/x/crypto v0.7.0
golang.org/x/sync v0.1.0
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand Down
Loading

0 comments on commit 227f178

Please sign in to comment.