1package serve
  2
  3import (
  4	"context"
  5	"fmt"
  6	"os"
  7	"os/signal"
  8	"path/filepath"
  9	"syscall"
 10	"time"
 11
 12	"github.com/charmbracelet/soft-serve/cmd"
 13	"github.com/charmbracelet/soft-serve/pkg/backend"
 14	"github.com/charmbracelet/soft-serve/pkg/config"
 15	"github.com/charmbracelet/soft-serve/pkg/db"
 16	"github.com/charmbracelet/soft-serve/pkg/db/migrate"
 17	"github.com/spf13/cobra"
 18)
 19
 20var (
 21	syncHooks bool
 22
 23	// Command is the serve command.
 24	Command = &cobra.Command{
 25		Use:                "serve",
 26		Short:              "Start the server",
 27		Args:               cobra.NoArgs,
 28		PersistentPreRunE:  cmd.InitBackendContext,
 29		PersistentPostRunE: cmd.CloseDBContext,
 30		RunE: func(c *cobra.Command, _ []string) error {
 31			ctx := c.Context()
 32			cfg := config.DefaultConfig()
 33			if cfg.Exist() {
 34				if err := cfg.ParseFile(); err != nil {
 35					return fmt.Errorf("parse config file: %w", err)
 36				}
 37			} else {
 38				if err := cfg.WriteConfig(); err != nil {
 39					return fmt.Errorf("write config file: %w", err)
 40				}
 41			}
 42
 43			if err := cfg.ParseEnv(); err != nil {
 44				return fmt.Errorf("parse environment variables: %w", err)
 45			}
 46
 47			// Create custom hooks directory if it doesn't exist
 48			customHooksPath := filepath.Join(cfg.DataPath, "hooks")
 49			if _, err := os.Stat(customHooksPath); err != nil && os.IsNotExist(err) {
 50				os.MkdirAll(customHooksPath, os.ModePerm) // nolint: errcheck
 51				// Generate update hook example without executable permissions
 52				hookPath := filepath.Join(customHooksPath, "update.sample")
 53				// nolint: gosec
 54				if err := os.WriteFile(hookPath, []byte(updateHookExample), 0o744); err != nil {
 55					return fmt.Errorf("failed to generate update hook example: %w", err)
 56				}
 57			}
 58
 59			// Create log directory if it doesn't exist
 60			logPath := filepath.Join(cfg.DataPath, "log")
 61			if _, err := os.Stat(logPath); err != nil && os.IsNotExist(err) {
 62				os.MkdirAll(logPath, os.ModePerm) // nolint: errcheck
 63			}
 64
 65			db := db.FromContext(ctx)
 66			if err := migrate.Migrate(ctx, db); err != nil {
 67				return fmt.Errorf("migration error: %w", err)
 68			}
 69
 70			s, err := NewServer(ctx)
 71			if err != nil {
 72				return fmt.Errorf("start server: %w", err)
 73			}
 74
 75			if syncHooks {
 76				be := backend.FromContext(ctx)
 77				if err := cmd.InitializeHooks(ctx, cfg, be); err != nil {
 78					return fmt.Errorf("initialize hooks: %w", err)
 79				}
 80			}
 81
 82			done := make(chan os.Signal, 1)
 83			lch := make(chan error, 1)
 84			go func() {
 85				defer close(lch)
 86				defer close(done)
 87				lch <- s.Start()
 88			}()
 89
 90			signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
 91			<-done
 92
 93			ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
 94			defer cancel()
 95			if err := s.Shutdown(ctx); err != nil {
 96				return err
 97			}
 98
 99			// wait for serve to finish
100			return <-lch
101		},
102	}
103)
104
105func init() {
106	Command.Flags().BoolVarP(&syncHooks, "sync-hooks", "", false, "synchronize hooks for all repositories before running the server")
107}
108
109const updateHookExample = `#!/bin/sh
110#
111# An example hook script to echo information about the push
112# and send it to the client.
113#
114# To enable this hook, rename this file to "update" and make it executable.
115
116refname="$1"
117oldrev="$2"
118newrev="$3"
119
120# Safety check
121if [ -z "$GIT_DIR" ]; then
122        echo "Don't run this script from the command line." >&2
123        echo " (if you want, you could supply GIT_DIR then run" >&2
124        echo "  $0 <ref> <oldrev> <newrev>)" >&2
125        exit 1
126fi
127
128if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
129        echo "usage: $0 <ref> <oldrev> <newrev>" >&2
130        exit 1
131fi
132
133# Check types
134# if $newrev is 0000...0000, it's a commit to delete a ref.
135zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
136if [ "$newrev" = "$zero" ]; then
137        newrev_type=delete
138else
139        newrev_type=$(git cat-file -t $newrev)
140fi
141
142echo "Hi from Soft Serve update hook!"
143echo
144echo "Repository: $SOFT_SERVE_REPO_NAME"
145echo "RefName: $refname"
146echo "Change Type: $newrev_type"
147echo "Old SHA1: $oldrev"
148echo "New SHA1: $newrev"
149
150exit 0
151`