Detailed changes
@@ -1,15 +1,23 @@
package main
import (
+ "bufio"
+ "bytes"
+ "context"
"fmt"
"os"
- "path/filepath"
"strings"
- "github.com/charmbracelet/keygen"
+ "github.com/charmbracelet/soft-serve/server/backend"
+ "github.com/charmbracelet/soft-serve/server/backend/sqlite"
"github.com/charmbracelet/soft-serve/server/config"
+ "github.com/charmbracelet/soft-serve/server/hooks"
"github.com/spf13/cobra"
- gossh "golang.org/x/crypto/ssh"
+)
+
+var (
+ confixCtxKey = "config"
+ backendCtxKey = "backend"
)
var (
@@ -20,94 +28,113 @@ var (
Short: "Run git server hooks",
Long: "Handles Soft Serve git server hooks.",
Hidden: true,
- RunE: func(_ *cobra.Command, args []string) error {
- c, s, err := commonInit()
+ PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
+ cfg, err := config.ParseConfig(configPath)
if err != nil {
- return err
+ return fmt.Errorf("could not parse config: %w", err)
}
- defer c.Close() //nolint:errcheck
- defer s.Close() //nolint:errcheck
- s.Stdin = os.Stdin
- s.Stdout = os.Stdout
- s.Stderr = os.Stderr
- cmd := fmt.Sprintf("hook %s", strings.Join(args, " "))
- if err := s.Run(cmd); err != nil {
- return err
+
+ // Set up the backend
+ // TODO: support other backends
+ sb, err := sqlite.NewSqliteBackend(cmd.Context(), cfg)
+ if err != nil {
+ return fmt.Errorf("failed to create sqlite backend: %w", err)
}
+
+ cfg = cfg.WithBackend(sb)
+
+ cmd.SetContext(context.WithValue(cmd.Context(), confixCtxKey, cfg))
+ cmd.SetContext(context.WithValue(cmd.Context(), backendCtxKey, sb))
+
return nil
},
}
-)
-func init() {
- hookCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "path to config file")
-}
+ hooksRunE = func(cmd *cobra.Command, args []string) error {
+ cfg := cmd.Context().Value(confixCtxKey).(*config.Config)
+ hks := cfg.Backend.(backend.Hooks)
-// TODO: use ssh controlmaster
-func commonInit() (c *gossh.Client, s *gossh.Session, err error) {
- cfg, err := config.ParseConfig(configPath)
- if err != nil {
- return
- }
+ // This is set in the server before invoking git-receive-pack/git-upload-pack
+ repoName := os.Getenv("SOFT_SERVE_REPO_NAME")
- // 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
- }
+ in := cmd.InOrStdin()
+ out := cmd.OutOrStdout()
+ err := cmd.ErrOrStderr()
- rs, err := filepath.Abs(filepath.Join(cfg.DataPath, "repos"))
- if err != nil {
- return
- }
+ cmdName := cmd.Name()
+ switch cmdName {
+ case hooks.PreReceiveHook, hooks.PostReceiveHook:
+ var buf bytes.Buffer
+ opts := make([]backend.HookArg, 0)
+ scanner := bufio.NewScanner(in)
+ for scanner.Scan() {
+ buf.Write(scanner.Bytes())
+ fields := strings.Fields(scanner.Text())
+ if len(fields) != 3 {
+ return fmt.Errorf("invalid pre-receive hook input: %s", scanner.Text())
+ }
+ opts = append(opts, backend.HookArg{
+ OldSha: fields[0],
+ NewSha: fields[1],
+ RefName: fields[2],
+ })
+ }
- 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, string(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
-}
+ switch cmdName {
+ case hooks.PreReceiveHook:
+ hks.PreReceive(out, err, repoName, opts)
+ case hooks.PostReceiveHook:
+ hks.PostReceive(out, err, repoName, opts)
+ }
+ case hooks.UpdateHook:
+ if len(args) != 3 {
+ return fmt.Errorf("invalid update hook input: %s", args)
+ }
-func newClient(cfg *config.Config) (*gossh.Client, error) {
- // Only accept the server's host key.
- pk, err := keygen.New(cfg.Internal.KeyPath, keygen.WithKeyType(keygen.Ed25519))
- if err != nil {
- return nil, err
+ hks.Update(out, err, repoName, backend.HookArg{
+ OldSha: args[0],
+ NewSha: args[1],
+ RefName: args[2],
+ })
+ case hooks.PostUpdateHook:
+ hks.PostUpdate(out, err, repoName, args...)
+ }
+
+ return nil
}
- ik, err := keygen.New(cfg.Internal.InternalKeyPath, keygen.WithKeyType(keygen.Ed25519))
- if err != nil {
- return nil, err
+
+ preReceiveCmd = &cobra.Command{
+ Use: "pre-receive",
+ Short: "Run git pre-receive hook",
+ RunE: hooksRunE,
}
- cc := &gossh.ClientConfig{
- User: "internal",
- Auth: []gossh.AuthMethod{
- gossh.PublicKeys(ik.Signer()),
- },
- HostKeyCallback: gossh.FixedHostKey(pk.PublicKey()),
+
+ updateCmd = &cobra.Command{
+ Use: "update",
+ Short: "Run git update hook",
+ Args: cobra.ExactArgs(3),
+ RunE: hooksRunE,
}
- c, err := gossh.Dial("tcp", cfg.Internal.ListenAddr, cc)
- if err != nil {
- return nil, err
+
+ postReceiveCmd = &cobra.Command{
+ Use: "post-receive",
+ Short: "Run git post-receive hook",
+ RunE: hooksRunE,
}
- return c, nil
-}
-func newSession(c *gossh.Client) (*gossh.Session, error) {
- s, err := c.NewSession()
- if err != nil {
- return nil, err
+ postUpdateCmd = &cobra.Command{
+ Use: "post-update",
+ Short: "Run git post-update hook",
+ RunE: hooksRunE,
}
- return s, nil
+)
+
+func init() {
+ hookCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "path to config file")
+ hookCmd.AddCommand(
+ preReceiveCmd,
+ updateCmd,
+ postReceiveCmd,
+ postUpdateCmd,
+ )
}
@@ -16,6 +16,7 @@ type Backend interface {
RepositoryAccess
UserStore
UserAccess
+ Hooks
}
// ParseAuthorizedKey parses an authorized key string into a public key.
@@ -0,0 +1,20 @@
+package backend
+
+import (
+ "io"
+)
+
+// HookArg is an argument to a git hook.
+type HookArg struct {
+ OldSha string
+ NewSha string
+ RefName string
+}
+
+// Hooks provides an interface for git server-side hooks.
+type Hooks interface {
+ PreReceive(stdout io.Writer, stderr io.Writer, repo string, args []HookArg)
+ Update(stdout io.Writer, stderr io.Writer, repo string, arg HookArg)
+ PostReceive(stdout io.Writer, stderr io.Writer, repo string, args []HookArg)
+ PostUpdate(stdout io.Writer, stderr io.Writer, repo string, args ...string)
+}
@@ -29,8 +29,6 @@ type RepositoryStore interface {
DeleteRepository(name string) error
// RenameRepository renames a repository.
RenameRepository(oldName, newName string) error
- // InitializeHooks initializes the hooks for the given repository.
- InitializeHooks(repo string) error
}
// RepositoryMetadata is an interface for managing repository metadata.
@@ -0,0 +1,64 @@
+package sqlite
+
+import (
+ "io"
+ "sync"
+
+ "github.com/charmbracelet/log"
+ "github.com/charmbracelet/soft-serve/server/backend"
+)
+
+// PostReceive is called by the git post-receive hook.
+//
+// It implements Hooks.
+func (d *SqliteBackend) PostReceive(stdout io.Writer, stderr io.Writer, repo string, args []backend.HookArg) {
+ log.WithPrefix("backend.sqlite.hooks").Debug("post-receive hook called", "repo", repo, "args", args)
+}
+
+// PreReceive is called by the git pre-receive hook.
+//
+// It implements Hooks.
+func (d *SqliteBackend) PreReceive(stdout io.Writer, stderr io.Writer, repo string, args []backend.HookArg) {
+ log.WithPrefix("backend.sqlite.hooks").Debug("pre-receive hook called", "repo", repo, "args", args)
+}
+
+// Update is called by the git update hook.
+//
+// It implements Hooks.
+func (d *SqliteBackend) Update(stdout io.Writer, stderr io.Writer, repo string, arg backend.HookArg) {
+ log.WithPrefix("backend.sqlite.hooks").Debug("update hook called", "repo", repo, "arg", arg)
+}
+
+// PostUpdate is called by the git post-update hook.
+//
+// It implements Hooks.
+func (d *SqliteBackend) PostUpdate(stdout io.Writer, stderr io.Writer, repo string, args ...string) {
+ log.WithPrefix("backend.sqlite.hooks").Debug("post-update hook called", "repo", repo, "args", args)
+
+ var wg sync.WaitGroup
+
+ // Update server info
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+
+ rr, err := d.Repository(repo)
+ if err != nil {
+ log.WithPrefix("backend.sqlite.hooks").Error("error getting repository", "repo", repo, "err", err)
+ return
+ }
+
+ r, err := rr.Open()
+ if err != nil {
+ log.WithPrefix("backend.sqlite.hooks").Error("error opening repository", "repo", repo, "err", err)
+ return
+ }
+
+ if err := r.UpdateServerInfo(); err != nil {
+ log.WithPrefix("backend.sqlite.hooks").Error("error updating server-info", "repo", repo, "err", err)
+ return
+ }
+ }()
+
+ wg.Wait()
+}
@@ -1,18 +1,17 @@
package sqlite
import (
- "bytes"
"context"
"fmt"
"os"
"path/filepath"
"strings"
- "text/template"
"github.com/charmbracelet/log"
"github.com/charmbracelet/soft-serve/git"
"github.com/charmbracelet/soft-serve/server/backend"
"github.com/charmbracelet/soft-serve/server/config"
+ "github.com/charmbracelet/soft-serve/server/hooks"
"github.com/charmbracelet/soft-serve/server/utils"
"github.com/jmoiron/sqlx"
_ "modernc.org/sqlite"
@@ -165,7 +164,7 @@ func (d *SqliteBackend) CreateRepository(name string, opts backend.RepositoryOpt
db: d.db,
}
- return r, d.InitializeHooks(name)
+ return r, d.initRepo(name)
}
// ImportRepository imports a repository from remote.
@@ -186,7 +185,7 @@ func (d *SqliteBackend) ImportRepository(name string, remote string, opts backen
Envs: []string{
fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile="%s" -o StrictHostKeyChecking=no -i "%s"`,
filepath.Join(d.cfg.DataPath, "ssh", "known_hosts"),
- d.cfg.Internal.ClientKeyPath,
+ d.cfg.SSH.ClientKeyPath,
),
},
},
@@ -551,157 +550,8 @@ func (d *SqliteBackend) RemoveCollaborator(repo string, username string) error {
)
}
-var (
- hookNames = []string{"pre-receive", "update", "post-update", "post-receive"}
- hookTpls = []string{
- // for pre-receive
- `#!/usr/bin/env bash
-# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY
-data=$(cat)
-exitcodes=""
-hookname=$(basename $0)
-GIT_DIR=${GIT_DIR:-$(dirname $0)/..}
-for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
- test -x "${hook}" && test -f "${hook}" || continue
- echo "${data}" | "${hook}"
- exitcodes="${exitcodes} $?"
-done
-for i in ${exitcodes}; do
- [ ${i} -eq 0 ] || exit ${i}
-done
-`,
-
- // for update
- `#!/usr/bin/env bash
-# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY
-exitcodes=""
-hookname=$(basename $0)
-GIT_DIR=${GIT_DIR:-$(dirname $0/..)}
-for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
- test -x "${hook}" && test -f "${hook}" || continue
- "${hook}" $1 $2 $3
- exitcodes="${exitcodes} $?"
-done
-for i in ${exitcodes}; do
- [ ${i} -eq 0 ] || exit ${i}
-done
-`,
-
- // for post-update
- `#!/usr/bin/env bash
-# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY
-data=$(cat)
-exitcodes=""
-hookname=$(basename $0)
-GIT_DIR=${GIT_DIR:-$(dirname $0)/..}
-for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
- test -x "${hook}" && test -f "${hook}" || continue
- "${hook}" $@
- exitcodes="${exitcodes} $?"
-done
-for i in ${exitcodes}; do
- [ ${i} -eq 0 ] || exit ${i}
-done
-`,
-
- // for post-receive
- `#!/usr/bin/env bash
-# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY
-data=$(cat)
-exitcodes=""
-hookname=$(basename $0)
-GIT_DIR=${GIT_DIR:-$(dirname $0)/..}
-for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
- test -x "${hook}" && test -f "${hook}" || continue
- echo "${data}" | "${hook}"
- exitcodes="${exitcodes} $?"
-done
-for i in ${exitcodes}; do
- [ ${i} -eq 0 ] || exit ${i}
-done
-`,
- }
-)
-
-// InitializeHooks updates the hooks for the given repository.
-//
-// It implements backend.Backend.
-func (d *SqliteBackend) InitializeHooks(repo string) 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 }} hook --config "{{ .Config }}" {{ .Hook }} {{ .Args }}
-`)
- if err != nil {
- return err
- }
-
- repo = utils.SanitizeRepo(repo) + ".git"
- hooksPath := filepath.Join(d.reposPath(), repo, "hooks")
- if err := os.MkdirAll(hooksPath, os.ModePerm); err != nil {
- return err
- }
-
- ex, err := os.Executable()
- if err != nil {
- return err
- }
-
- dp, err := filepath.Abs(d.dp)
- if err != nil {
- return fmt.Errorf("failed to get absolute path for data path: %w", err)
- }
-
- cp := filepath.Join(dp, "config.yaml")
- envs := []string{}
- for i, hook := range hookNames {
- var data bytes.Buffer
- var args string
- hp := filepath.Join(hooksPath, hook)
- if err := os.WriteFile(hp, []byte(hookTpls[i]), os.ModePerm); err != nil {
- return err
- }
-
- // Create hook.d directory.
- hp += ".d"
- if err := os.MkdirAll(hp, os.ModePerm); err != nil {
- return err
- }
-
- if hook == "update" {
- args = "$1 $2 $3"
- } else if hook == "post-update" {
- args = "$@"
- }
-
- err = hookTmpl.Execute(&data, struct {
- Executable string
- Hook string
- Args string
- Envs []string
- Config string
- }{
- Executable: ex,
- Hook: hook,
- Args: args,
- Envs: envs,
- Config: cp,
- })
- if err != nil {
- logger.Error("failed to execute hook template", "err", err)
- continue
- }
-
- hp = filepath.Join(hp, "soft-serve")
- err = os.WriteFile(hp, data.Bytes(), os.ModePerm) //nolint:gosec
- if err != nil {
- logger.Error("failed to write hook", "err", err)
- continue
- }
- }
-
- return nil
+func (d *SqliteBackend) initRepo(repo string) error {
+ return hooks.GenerateHooks(d.ctx, d.cfg, repo)
}
func (d *SqliteBackend) initRepos() error {
@@ -711,7 +561,7 @@ func (d *SqliteBackend) initRepos() error {
}
for _, repo := range repos {
- if err := d.InitializeHooks(repo.Name()); err != nil {
+ if err := d.initRepo(repo.Name()); err != nil {
return err
}
}
@@ -25,6 +25,9 @@ type SSHConfig struct {
// KeyPath is the path to the SSH server's private key.
KeyPath string `env:"KEY_PATH" yaml:"key_path"`
+ // ClientKeyPath is the path to the server's client private key.
+ ClientKeyPath string `env:"CLIENT_KEY_PATH" yaml:"client_key_path"`
+
// MaxTimeout is the maximum number of seconds a connection can take.
MaxTimeout int `env:"MAX_TIMEOUT" yaml:"max_timeout"`
@@ -68,22 +71,6 @@ type StatsConfig struct {
ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"`
}
-// InternalConfig is the configuration for the internal server.
-// This is used for internal communication between the Soft Serve client and server.
-type InternalConfig struct {
- // ListenAddr is the address on which the internal server will listen.
- ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"`
-
- // KeyPath is the path to the SSH server's host private key.
- KeyPath string `env:"KEY_PATH" yaml:"key_path"`
-
- // InternalKeyPath is the path to the server's internal private key.
- InternalKeyPath string `env:"INTERNAL_KEY_PATH" yaml:"internal_key_path"`
-
- // ClientKeyPath is the path to the server's client private key.
- ClientKeyPath string `env:"CLIENT_KEY_PATH" yaml:"client_key_path"`
-}
-
// Config is the configuration for Soft Serve.
type Config struct {
// Name is the name of the server.
@@ -101,9 +88,6 @@ type Config struct {
// Stats is the configuration for the stats server.
Stats StatsConfig `envPrefix:"STATS_" yaml:"stats"`
- // Internal is the configuration for the internal server.
- Internal InternalConfig `envPrefix:"INTERNAL_" yaml:"internal"`
-
// InitialAdminKeys is a list of public keys that will be added to the list of admins.
InitialAdminKeys []string `env:"INITIAL_ADMIN_KEYS" envSeparator:"\n" yaml:"initial_admin_keys"`
@@ -120,11 +104,12 @@ func parseConfig(path string) (*Config, error) {
Name: "Soft Serve",
DataPath: dataPath,
SSH: SSHConfig{
- ListenAddr: ":23231",
- PublicURL: "ssh://localhost:23231",
- KeyPath: filepath.Join("ssh", "soft_serve_host_ed25519"),
- MaxTimeout: 0,
- IdleTimeout: 0,
+ ListenAddr: ":23231",
+ PublicURL: "ssh://localhost:23231",
+ KeyPath: filepath.Join("ssh", "soft_serve_host_ed25519"),
+ ClientKeyPath: filepath.Join("ssh", "soft_serve_client_ed25519"),
+ MaxTimeout: 0,
+ IdleTimeout: 0,
},
Git: GitConfig{
ListenAddr: ":9418",
@@ -139,12 +124,6 @@ func parseConfig(path string) (*Config, error) {
Stats: StatsConfig{
ListenAddr: "localhost:23233",
},
- Internal: InternalConfig{
- ListenAddr: "localhost:23230",
- KeyPath: filepath.Join("ssh", "soft_serve_internal_host_ed25519"),
- InternalKeyPath: filepath.Join("ssh", "soft_serve_internal_ed25519"),
- ClientKeyPath: filepath.Join("ssh", "soft_serve_client_ed25519"),
- },
}
f, err := os.Open(path)
@@ -260,16 +239,8 @@ func (c *Config) validate() error {
c.SSH.KeyPath = filepath.Join(c.DataPath, c.SSH.KeyPath)
}
- if c.Internal.KeyPath != "" && !filepath.IsAbs(c.Internal.KeyPath) {
- c.Internal.KeyPath = filepath.Join(c.DataPath, c.Internal.KeyPath)
- }
-
- if c.Internal.ClientKeyPath != "" && !filepath.IsAbs(c.Internal.ClientKeyPath) {
- c.Internal.ClientKeyPath = filepath.Join(c.DataPath, c.Internal.ClientKeyPath)
- }
-
- if c.Internal.InternalKeyPath != "" && !filepath.IsAbs(c.Internal.InternalKeyPath) {
- c.Internal.InternalKeyPath = filepath.Join(c.DataPath, c.Internal.InternalKeyPath)
+ if c.SSH.ClientKeyPath != "" && !filepath.IsAbs(c.SSH.ClientKeyPath) {
+ c.SSH.ClientKeyPath = filepath.Join(c.DataPath, c.SSH.ClientKeyPath)
}
if c.HTTP.TLSKeyPath != "" && !filepath.IsAbs(c.HTTP.TLSKeyPath) {
@@ -298,7 +269,7 @@ func parseAuthKeys(aks []string) []ssh.PublicKey {
return pks
}
-// AdminKeys returns the admin keys including the internal api key.
+// AdminKeys returns the server admin keys.
func (c *Config) AdminKeys() []ssh.PublicKey {
- return parseAuthKeys(append(c.InitialAdminKeys, c.Internal.InternalKeyPath))
+ return parseAuthKeys(c.InitialAdminKeys)
}
@@ -24,6 +24,10 @@ ssh:
# The path to the SSH server's private key.
key_path: "{{ .SSH.KeyPath }}"
+ # The path to the server's client private key. This key will be used to
+ # authenticate the server to make git requests to ssh remotes.
+ client_key_path: "{{ .Internal.ClientKeyPath }}"
+
# The maximum number of seconds a connection can take.
# A value of 0 means no timeout.
max_timeout: {{ .SSH.MaxTimeout }}
@@ -68,22 +72,6 @@ stats:
# The address on which the stats server will listen.
listen_addr: "{{ .Stats.ListenAddr }}"
-# The internal server configuration.
-internal:
- # The address on which the internal server will listen.
- listen_addr: "{{ .Internal.ListenAddr }}"
-
- # The path to the Internal server's host private key.
- key_path: "{{ .Internal.KeyPath }}"
-
- # The path to the Internal server's client private key.
- # This key will be used to authenticate the server to make git requests to
- # ssh remotes.
- client_key_path: "{{ .Internal.ClientKeyPath }}"
-
- # The path to the Internal server's internal api private key.
- internal_key_path: "{{ .Internal.InternalKeyPath }}"
-
# Additional admin keys.
#initial_admin_keys:
# - "ssh-rsa AAAAB3NzaC1yc2..."
@@ -253,7 +253,13 @@ func (d *GitDaemon) handleClient(conn net.Conn) {
return
}
- if err := gitPack(c, c, c, filepath.Join(reposDir, repo)); err != nil {
+ // Environment variables to pass down to git hooks.
+ envs := []string{
+ "SOFT_SERVE_REPO_NAME=" + name,
+ "SOFT_SERVE_REPO_PATH=" + filepath.Join(reposDir, repo),
+ }
+
+ if err := gitPack(ctx, c, c, c, filepath.Join(reposDir, repo), envs...); err != nil {
fatal(c, err)
return
}
@@ -1,16 +1,19 @@
package git
import (
+ "context"
"errors"
"fmt"
"io"
"os"
+ "os/exec"
"path/filepath"
"strings"
"github.com/charmbracelet/log"
"github.com/charmbracelet/soft-serve/git"
"github.com/go-git/go-git/v5/plumbing/format/pktline"
+ "golang.org/x/sync/errgroup"
)
var (
@@ -42,7 +45,7 @@ const (
)
// UploadPack runs the git upload-pack protocol against the provided repo.
-func UploadPack(in io.Reader, out io.Writer, er io.Writer, repoDir string) error {
+func UploadPack(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, repoDir string, envs ...string) error {
exists, err := fileExists(repoDir)
if !exists {
return ErrInvalidRepo
@@ -50,11 +53,11 @@ func UploadPack(in io.Reader, out io.Writer, er io.Writer, repoDir string) error
if err != nil {
return err
}
- return RunGit(in, out, er, "", UploadPackBin[4:], repoDir)
+ return RunGit(ctx, in, out, er, "", envs, UploadPackBin[4:], repoDir)
}
// UploadArchive runs the git upload-archive protocol against the provided repo.
-func UploadArchive(in io.Reader, out io.Writer, er io.Writer, repoDir string) error {
+func UploadArchive(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, repoDir string, envs ...string) error {
exists, err := fileExists(repoDir)
if !exists {
return ErrInvalidRepo
@@ -62,25 +65,77 @@ func UploadArchive(in io.Reader, out io.Writer, er io.Writer, repoDir string) er
if err != nil {
return err
}
- return RunGit(in, out, er, "", UploadArchiveBin[4:], repoDir)
+ return RunGit(ctx, in, out, er, "", envs, UploadArchiveBin[4:], repoDir)
}
// ReceivePack runs the git receive-pack protocol against the provided repo.
-func ReceivePack(in io.Reader, out io.Writer, er io.Writer, repoDir string) error {
- if err := RunGit(in, out, er, "", ReceivePackBin[4:], repoDir); err != nil {
+func ReceivePack(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, repoDir string, envs ...string) error {
+ if err := RunGit(ctx, in, out, er, "", envs, ReceivePackBin[4:], repoDir); err != nil {
return err
}
- return EnsureDefaultBranch(in, out, er, repoDir)
+ return EnsureDefaultBranch(ctx, in, out, er, repoDir)
}
// RunGit runs a git command in the given repo.
-func RunGit(in io.Reader, out io.Writer, err io.Writer, dir string, args ...string) error {
- c := git.NewCommand(args...)
- return c.RunInDirWithOptions(dir, git.RunInDirOptions{
- Stdin: in,
- Stdout: out,
- Stderr: err,
+func RunGit(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, dir string, envs []string, args ...string) error {
+ logger := log.WithPrefix("server.git")
+ c := exec.CommandContext(ctx, "git", args...)
+ c.Dir = dir
+ c.Env = append(c.Env, envs...)
+ c.Env = append(c.Env, "SOFT_SERVE_DEBUG="+os.Getenv("SOFT_SERVE_DEBUG"))
+ c.Env = append(c.Env, "PATH="+os.Getenv("PATH"))
+
+ stdin, err := c.StdinPipe()
+ if err != nil {
+ logger.Error("failed to get stdin pipe", "err", err)
+ return err
+ }
+
+ stdout, err := c.StdoutPipe()
+ if err != nil {
+ logger.Error("failed to get stdout pipe", "err", err)
+ return err
+ }
+
+ stderr, err := c.StderrPipe()
+ if err != nil {
+ logger.Error("failed to get stderr pipe", "err", err)
+ return err
+ }
+
+ if err := c.Start(); err != nil {
+ logger.Error("failed to start command", "err", err)
+ return err
+ }
+
+ errg, ctx := errgroup.WithContext(ctx)
+
+ // stdin
+ errg.Go(func() error {
+ defer stdin.Close()
+
+ _, err := io.Copy(stdin, in)
+ return err
+ })
+
+ // stdout
+ errg.Go(func() error {
+ _, err := io.Copy(out, stdout)
+ return err
+ })
+
+ // stderr
+ errg.Go(func() error {
+ _, err := io.Copy(er, stderr)
+ return err
})
+
+ if err := errg.Wait(); err != nil {
+ logger.Error("while running git command", "err", err)
+ return err
+ }
+
+ return nil
}
// WritePktline encodes and writes a pktline to the given writer.
@@ -129,7 +184,7 @@ func fileExists(path string) (bool, error) {
return true, err
}
-func EnsureDefaultBranch(in io.Reader, out io.Writer, er io.Writer, repoPath string) error {
+func EnsureDefaultBranch(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, repoPath string) error {
r, err := git.Open(repoPath)
if err != nil {
return err
@@ -144,7 +199,7 @@ func EnsureDefaultBranch(in io.Reader, out io.Writer, er io.Writer, repoPath str
// Rename the default branch to the first branch available
_, err = r.HEAD()
if err == git.ErrReferenceNotExist {
- err = RunGit(in, out, er, repoPath, "branch", "-M", brs[0])
+ err = RunGit(ctx, in, out, er, repoPath, []string{}, "branch", "-M", brs[0])
if err != nil {
return err
}
@@ -1,54 +0,0 @@
-package server
-
-import (
- "io"
-
- "github.com/charmbracelet/log"
- "github.com/charmbracelet/soft-serve/server/hooks"
-)
-
-var _ hooks.Hooks = (*Server)(nil)
-
-// PostReceive is called by the git post-receive hook.
-//
-// It implements Hooks.
-func (*Server) PostReceive(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args []hooks.HookArg) {
- log.WithPrefix("server.hooks").Debug("post-receive hook called", "repo", repo, "args", args)
-}
-
-// PreReceive is called by the git pre-receive hook.
-//
-// It implements Hooks.
-func (*Server) PreReceive(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args []hooks.HookArg) {
- log.WithPrefix("server.hooks").Debug("pre-receive hook called", "repo", repo, "args", args)
-}
-
-// Update is called by the git update hook.
-//
-// It implements Hooks.
-func (*Server) Update(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, arg hooks.HookArg) {
- log.WithPrefix("server.hooks").Debug("update hook called", "repo", repo, "arg", arg)
-}
-
-// PostUpdate is called by the git post-update hook.
-//
-// It implements Hooks.
-func (s *Server) PostUpdate(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args ...string) {
- log.WithPrefix("server.hooks").Debug("post-update hook called", "repo", repo, "args", args)
- rr, err := s.Config.Backend.Repository(repo)
- if err != nil {
- log.WithPrefix("server.hooks.post-update").Error("error getting repository", "repo", repo, "err", err)
- return
- }
-
- r, err := rr.Open()
- if err != nil {
- log.WithPrefix("server.hooks.post-update").Error("error opening repository", "repo", repo, "err", err)
- return
- }
-
- if err := r.UpdateServerInfo(); err != nil {
- log.WithPrefix("server.hooks.post-update").Error("error updating server info", "repo", repo, "err", err)
- return
- }
-}
@@ -1,18 +1,152 @@
package hooks
-import "io"
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "text/template"
-// HookArg is an argument to a git hook.
-type HookArg struct {
- OldSha string
- NewSha string
- RefName string
-}
+ "github.com/charmbracelet/log"
+ "github.com/charmbracelet/soft-serve/server/config"
+ "github.com/charmbracelet/soft-serve/server/utils"
+)
+
+// The names of git server-side hooks.
+const (
+ PreReceiveHook = "pre-receive"
+ UpdateHook = "update"
+ PostReceiveHook = "post-receive"
+ PostUpdateHook = "post-update"
+)
+
+// GenerateHooks generates git server-side hooks for a repository. Currently, it supports the following hooks:
+// - pre-receive
+// - update
+// - post-receive
+// - post-update
+//
+// This function should be called by the backend when a repository is created.
+// TODO: support context
+func GenerateHooks(ctx context.Context, cfg *config.Config, repo string) error {
+ repo = utils.SanitizeRepo(repo) + ".git"
+ hooksPath := filepath.Join(cfg.DataPath, "repos", repo, "hooks")
+ if err := os.MkdirAll(hooksPath, os.ModePerm); err != nil {
+ return err
+ }
+
+ ex, err := os.Executable()
+ if err != nil {
+ return err
+ }
+
+ dp, err := filepath.Abs(cfg.DataPath)
+ if err != nil {
+ return fmt.Errorf("failed to get absolute path for data path: %w", err)
+ }
+
+ cp := filepath.Join(dp, "config.yaml")
+ // Add extra environment variables to the hooks here.
+ envs := []string{}
+
+ for _, hook := range []string{
+ PreReceiveHook,
+ UpdateHook,
+ PostReceiveHook,
+ PostUpdateHook,
+ } {
+ var data bytes.Buffer
+ var args string
+
+ // Hooks script/directory path
+ hp := filepath.Join(hooksPath, hook)
+
+ // Write the hooks primary script
+ if err := os.WriteFile(hp, []byte(hookTemplate), os.ModePerm); err != nil {
+ return err
+ }
-// Hooks provides an interface for git server-side hooks.
-type Hooks interface {
- PreReceive(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args []HookArg)
- Update(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, arg HookArg)
- PostReceive(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args []HookArg)
- PostUpdate(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args ...string)
+ // Create ${hook}.d directory.
+ hp += ".d"
+ if err := os.MkdirAll(hp, os.ModePerm); err != nil {
+ return err
+ }
+
+ switch hook {
+ case UpdateHook:
+ args = "$1 $2 $3"
+ case PostUpdateHook:
+ args = "$@"
+ }
+
+ if err := hooksTmpl.Execute(&data, struct {
+ Executable string
+ Config string
+ Envs []string
+ Hook string
+ Args string
+ }{
+ Executable: ex,
+ Config: cp,
+ Envs: envs,
+ Hook: hook,
+ Args: args,
+ }); err != nil {
+ log.WithPrefix("backend.hooks").Error("failed to execute hook template", "err", err)
+ continue
+ }
+
+ // Write the soft-serve hook inside ${hook}.d directory.
+ hp = filepath.Join(hp, "soft-serve")
+ err = os.WriteFile(hp, data.Bytes(), os.ModePerm) //nolint:gosec
+ if err != nil {
+ log.WithPrefix("backend.hooks").Error("failed to write hook", "err", err)
+ continue
+ }
+ }
+
+ return nil
}
+
+const (
+ // hookTemplate allows us to run multiple hooks from a directory. It should
+ // support every type of git hook, as it proxies both stdin and arguments.
+ hookTemplate = `#!/usr/bin/env bash
+# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY
+data=$(cat)
+exitcodes=""
+hookname=$(basename $0)
+GIT_DIR=${GIT_DIR:-$(dirname $0)/..}
+for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
+ # Avoid running non-executable hooks
+ test -x "${hook}" && test -f "${hook}" || continue
+
+ # Run the actual hook
+ echo "${data}" | "${hook}" "$@"
+
+ # Store the exit code for later use
+ exitcodes="${exitcodes} $?"
+done
+
+# Exit on the first non-zero exit code.
+for i in ${exitcodes}; do
+ [ ${i} -eq 0 ] || exit ${i}
+done
+`
+)
+
+var (
+ // hooksTmpl is the soft-serve hook that will be run by the git hooks
+ // inside the hooks directory.
+ hooksTmpl = template.Must(template.New("hooks").Parse(`#!/usr/bin/env bash
+# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY
+if [ -z "$SOFT_SERVE_REPO_NAME" ]; then
+ echo "Warning: SOFT_SERVE_REPO_NAME not defined. Skipping hooks."
+ exit 0
+fi
+{{ range $_, $env := .Envs }}
+{{ $env }} \{{ end }}
+{{ .Executable }} hook --config "{{ .Config }}" {{ .Hook }} {{ .Args }}
+`))
+)
@@ -1,84 +0,0 @@
-package internal
-
-import (
- "context"
-
- "github.com/charmbracelet/soft-serve/server/config"
- "github.com/charmbracelet/soft-serve/server/hooks"
- "github.com/charmbracelet/ssh"
- "github.com/charmbracelet/wish"
- "github.com/spf13/cobra"
-)
-
-var (
- hooksCtxKey = "hooks"
- sessionCtxKey = "session"
- configCtxKey = "config"
-)
-
-// rootCommand is the root command for the server.
-func rootCommand(cfg *config.Config, s ssh.Session) *cobra.Command {
- rootCmd := &cobra.Command{
- Short: "Soft Serve internal API.",
- SilenceUsage: true,
- }
-
- rootCmd.SetIn(s)
- rootCmd.SetOut(s)
- rootCmd.SetErr(s)
- rootCmd.CompletionOptions.DisableDefaultCmd = true
-
- rootCmd.AddCommand(
- hookCommand(),
- )
-
- return rootCmd
-}
-
-// Middleware returns the middleware for the server.
-func (i *InternalServer) Middleware(hooks hooks.Hooks) wish.Middleware {
- return func(sh ssh.Handler) ssh.Handler {
- return func(s ssh.Session) {
- _, _, active := s.Pty()
- if active {
- return
- }
-
- // Ignore git server commands.
- args := s.Command()
- if len(args) > 0 {
- if args[0] == "git-receive-pack" ||
- args[0] == "git-upload-pack" ||
- args[0] == "git-upload-archive" {
- return
- }
- }
-
- ctx := context.WithValue(s.Context(), hooksCtxKey, hooks)
- ctx = context.WithValue(ctx, sessionCtxKey, s)
- ctx = context.WithValue(ctx, configCtxKey, i.cfg)
-
- rootCmd := rootCommand(i.cfg, s)
- rootCmd.SetArgs(args)
- if len(args) == 0 {
- // otherwise it'll default to os.Args, which is not what we want.
- rootCmd.SetArgs([]string{"--help"})
- }
- rootCmd.SetIn(s)
- rootCmd.SetOut(s)
- rootCmd.CompletionOptions.DisableDefaultCmd = true
- rootCmd.SetErr(s.Stderr())
- if err := rootCmd.ExecuteContext(ctx); err != nil {
- _ = s.Exit(1)
- }
- sh(s)
- }
- }
-}
-
-func fromContext(cmd *cobra.Command) (*config.Config, ssh.Session) {
- ctx := cmd.Context()
- cfg := ctx.Value(configCtxKey).(*config.Config)
- s := ctx.Value(sessionCtxKey).(ssh.Session)
- return cfg, s
-}
@@ -1,138 +0,0 @@
-package internal
-
-import (
- "bufio"
- "fmt"
- "strings"
-
- "github.com/charmbracelet/keygen"
- "github.com/charmbracelet/log"
- "github.com/charmbracelet/soft-serve/server/backend"
- "github.com/charmbracelet/soft-serve/server/errors"
- "github.com/charmbracelet/soft-serve/server/hooks"
- "github.com/charmbracelet/ssh"
- "github.com/spf13/cobra"
-)
-
-// hookCommand handles Soft Serve internal API git hook requests.
-func hookCommand() *cobra.Command {
- preReceiveCmd := &cobra.Command{
- Use: "pre-receive",
- Short: "Run git pre-receive hook",
- RunE: func(cmd *cobra.Command, args []string) error {
- _, s := fromContext(cmd)
- hks := cmd.Context().Value(hooksCtxKey).(hooks.Hooks)
- repoName := getRepoName(s)
- opts := make([]hooks.HookArg, 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, hooks.HookArg{
- OldSha: fields[0],
- NewSha: fields[1],
- RefName: fields[2],
- })
- }
- hks.PreReceive(s, s, s.Stderr(), 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 {
- _, s := fromContext(cmd)
- hks := cmd.Context().Value(hooksCtxKey).(hooks.Hooks)
- repoName := getRepoName(s)
- hks.Update(s, s, s.Stderr(), repoName, hooks.HookArg{
- 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, _ []string) error {
- _, s := fromContext(cmd)
- hks := cmd.Context().Value(hooksCtxKey).(hooks.Hooks)
- repoName := getRepoName(s)
- opts := make([]hooks.HookArg, 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, hooks.HookArg{
- OldSha: fields[0],
- NewSha: fields[1],
- RefName: fields[2],
- })
- }
- hks.PostReceive(s, s, s.Stderr(), repoName, opts)
- return nil
- },
- }
-
- postUpdateCmd := &cobra.Command{
- Use: "post-update",
- Short: "Run git post-update hook",
- RunE: func(cmd *cobra.Command, args []string) error {
- _, s := fromContext(cmd)
- hks := cmd.Context().Value(hooksCtxKey).(hooks.Hooks)
- repoName := getRepoName(s)
- hks.PostUpdate(s, s, s.Stderr(), repoName, args...)
- return nil
- },
- }
-
- hookCmd := &cobra.Command{
- Use: "hook",
- Short: "Run git server hooks",
- Hidden: true,
- SilenceUsage: true,
- }
-
- hookCmd.AddCommand(
- preReceiveCmd,
- updateCmd,
- postReceiveCmd,
- postUpdateCmd,
- )
-
- return hookCmd
-}
-
-// Check if the session's public key matches the internal API key.
-func checkIfInternal(cmd *cobra.Command, _ []string) error {
- cfg, s := fromContext(cmd)
- pk := s.PublicKey()
- kp, err := keygen.New(cfg.Internal.InternalKeyPath, keygen.WithKeyType(keygen.Ed25519))
- if err != nil {
- log.WithPrefix("server.internal").Errorf("failed to read internal key: %v", err)
- return err
- }
- if !backend.KeysEqual(pk, kp.PublicKey()) {
- return errors.ErrUnauthorized
- }
- return nil
-}
-
-func getRepoName(s ssh.Session) string {
- var repoName string
- for _, env := range s.Environ() {
- if strings.HasPrefix(env, "SOFT_SERVE_REPO_NAME=") {
- return strings.TrimPrefix(env, "SOFT_SERVE_REPO_NAME=")
- }
- }
- return repoName
-}
@@ -1,86 +0,0 @@
-package internal
-
-import (
- "context"
- "fmt"
-
- "github.com/charmbracelet/keygen"
- "github.com/charmbracelet/soft-serve/server/backend"
- "github.com/charmbracelet/soft-serve/server/config"
- "github.com/charmbracelet/soft-serve/server/hooks"
- "github.com/charmbracelet/ssh"
- "github.com/charmbracelet/wish"
-)
-
-// InternalServer is a internal interface to communicate with the server.
-type InternalServer struct {
- cfg *config.Config
- s *ssh.Server
- kp *keygen.SSHKeyPair
- ckp *keygen.SSHKeyPair
-}
-
-// NewInternalServer returns a new internal server.
-func NewInternalServer(cfg *config.Config, hooks hooks.Hooks) (*InternalServer, error) {
- i := &InternalServer{cfg: cfg}
-
- // Create internal key.
- ikp, err := keygen.New(
- cfg.Internal.InternalKeyPath,
- keygen.WithKeyType(keygen.Ed25519),
- keygen.WithWrite(),
- )
- if err != nil {
- return nil, fmt.Errorf("internal key: %w", err)
- }
-
- i.kp = ikp
-
- // Create client key.
- ckp, err := keygen.New(
- cfg.Internal.ClientKeyPath,
- keygen.WithKeyType(keygen.Ed25519),
- keygen.WithWrite(),
- )
- if err != nil {
- return nil, fmt.Errorf("client key: %w", err)
- }
-
- i.ckp = ckp
-
- s, err := wish.NewServer(
- wish.WithAddress(cfg.Internal.ListenAddr),
- wish.WithHostKeyPath(cfg.Internal.KeyPath),
- wish.WithPublicKeyAuth(i.PublicKeyHandler),
- wish.WithMiddleware(
- i.Middleware(hooks),
- ),
- )
- if err != nil {
- return nil, fmt.Errorf("wish: %w", err)
- }
-
- i.s = s
-
- return i, nil
-}
-
-// PublicKeyHandler handles public key authentication.
-func (i *InternalServer) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool {
- return backend.KeysEqual(i.kp.PublicKey(), pk)
-}
-
-// Start starts the internal server.
-func (i *InternalServer) Start() error {
- return i.s.ListenAndServe()
-}
-
-// Shutdown shuts down the internal server.
-func (i *InternalServer) Shutdown(ctx context.Context) error {
- return i.s.Shutdown(ctx)
-}
-
-// Close closes the internal server.
-func (i *InternalServer) Close() error {
- return i.s.Close()
-}
@@ -38,7 +38,7 @@ func mirrorJob(cfg *config.Config) func() {
cmd.AddEnvs(
fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile="%s" -o StrictHostKeyChecking=no -i "%s"`,
filepath.Join(cfg.DataPath, "ssh", "known_hosts"),
- cfg.Internal.ClientKeyPath,
+ cfg.SSH.ClientKeyPath,
),
)
if _, err := cmd.RunInDir(r.Path); err != nil {
@@ -13,7 +13,6 @@ import (
"github.com/charmbracelet/soft-serve/server/config"
"github.com/charmbracelet/soft-serve/server/cron"
"github.com/charmbracelet/soft-serve/server/daemon"
- "github.com/charmbracelet/soft-serve/server/internal"
sshsrv "github.com/charmbracelet/soft-serve/server/ssh"
"github.com/charmbracelet/soft-serve/server/stats"
"github.com/charmbracelet/soft-serve/server/web"
@@ -27,15 +26,14 @@ var (
// Server is the Soft Serve server.
type Server struct {
- SSHServer *sshsrv.SSHServer
- GitDaemon *daemon.GitDaemon
- HTTPServer *web.HTTPServer
- StatsServer *stats.StatsServer
- InternalServer *internal.InternalServer
- Cron *cron.CronScheduler
- Config *config.Config
- Backend backend.Backend
- ctx context.Context
+ SSHServer *sshsrv.SSHServer
+ GitDaemon *daemon.GitDaemon
+ HTTPServer *web.HTTPServer
+ StatsServer *stats.StatsServer
+ Cron *cron.CronScheduler
+ Config *config.Config
+ Backend backend.Backend
+ ctx context.Context
}
// NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
@@ -84,11 +82,6 @@ func NewServer(ctx context.Context, cfg *config.Config) (*Server, error) {
return nil, fmt.Errorf("create stats server: %w", err)
}
- srv.InternalServer, err = internal.NewInternalServer(cfg, srv)
- if err != nil {
- return nil, fmt.Errorf("create internal server: %w", err)
- }
-
return srv, nil
}
@@ -143,13 +136,6 @@ func (s *Server) Start() error {
s.Cron.Start()
return nil
})
- errg.Go(func() error {
- logger.Print("Starting internal server", "addr", s.Config.Internal.ListenAddr)
- if err := start(ctx, s.InternalServer.Start); !errors.Is(err, http.ErrServerClosed) {
- return err
- }
- return nil
- })
return errg.Wait()
}
@@ -172,9 +158,6 @@ func (s *Server) Shutdown(ctx context.Context) error {
s.Cron.Stop()
return nil
})
- errg.Go(func() error {
- return s.InternalServer.Shutdown(ctx)
- })
return errg.Wait()
}
@@ -189,6 +172,5 @@ func (s *Server) Close() error {
s.Cron.Stop()
return nil
})
- errg.Go(s.InternalServer.Close)
return errg.Wait()
}
@@ -189,6 +189,13 @@ func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
return
}
+ // Environment variables to pass down to git hooks.
+ envs := []string{
+ "SOFT_SERVE_REPO_NAME=" + name,
+ "SOFT_SERVE_REPO_PATH=" + filepath.Join(reposDir, repo),
+ "SOFT_SERVE_PUBLIC_KEY=" + ak,
+ }
+
logger.Debug("git middleware", "cmd", gc, "access", access.String())
repoDir := filepath.Join(reposDir, repo)
switch gc {
@@ -205,7 +212,7 @@ func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
}
createRepoCounter.WithLabelValues(ak, s.User(), name).Inc()
}
- if err := git.ReceivePack(s, s, s.Stderr(), repoDir); err != nil {
+ if err := git.ReceivePack(s.Context(), s, s, s.Stderr(), repoDir, envs...); err != nil {
sshFatal(s, git.ErrSystemMalfunction)
}
receivePackCounter.WithLabelValues(ak, s.User(), name).Inc()
@@ -223,7 +230,7 @@ func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware {
counter = uploadArchiveCounter
}
- err := gitPack(s, s, s.Stderr(), repoDir)
+ err := gitPack(s.Context(), s, s, s.Stderr(), repoDir, envs...)
if errors.Is(err, git.ErrInvalidRepo) {
sshFatal(s, git.ErrInvalidRepo)
} else if err != nil {
@@ -2,6 +2,8 @@ package test
import "net"
+// RandomPort returns a random port number.
+// This is mainly used for testing.
func RandomPort() int {
addr, _ := net.Listen("tcp", ":0") //nolint:gosec
_ = addr.Close()