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`