1package main
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io/fs"
8 "os"
9 "runtime/debug"
10 "strings"
11 "time"
12
13 "github.com/charmbracelet/log"
14 "github.com/charmbracelet/soft-serve/server/backend"
15 "github.com/charmbracelet/soft-serve/server/config"
16 "github.com/charmbracelet/soft-serve/server/db"
17 "github.com/charmbracelet/soft-serve/server/store"
18 "github.com/charmbracelet/soft-serve/server/store/database"
19 _ "github.com/lib/pq" // postgres driver
20 "github.com/spf13/cobra"
21 "go.uber.org/automaxprocs/maxprocs"
22
23 _ "modernc.org/sqlite" // sqlite driver
24)
25
26var (
27 // Version contains the application version number. It's set via ldflags
28 // when building.
29 Version = ""
30
31 // CommitSHA contains the SHA of the commit that this application was built
32 // against. It's set via ldflags when building.
33 CommitSHA = ""
34
35 rootCmd = &cobra.Command{
36 Use: "soft",
37 Short: "A self-hostable Git server for the command line",
38 Long: "Soft Serve is a self-hostable Git server for the command line.",
39 SilenceUsage: true,
40 }
41)
42
43func init() {
44 rootCmd.AddCommand(
45 serveCmd,
46 manCmd,
47 hookCmd,
48 migrateConfig,
49 adminCmd,
50 )
51 rootCmd.CompletionOptions.HiddenDefaultCmd = true
52
53 if len(CommitSHA) >= 7 {
54 vt := rootCmd.VersionTemplate()
55 rootCmd.SetVersionTemplate(vt[:len(vt)-1] + " (" + CommitSHA[0:7] + ")\n")
56 }
57 if Version == "" {
58 if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
59 Version = info.Main.Version
60 } else {
61 Version = "unknown (built from source)"
62 }
63 }
64 rootCmd.Version = Version
65}
66
67func main() {
68 ctx := context.Background()
69 cfg := config.DefaultConfig()
70 if cfg.Exist() {
71 if err := cfg.Parse(); err != nil {
72 log.Fatal(err)
73 }
74 }
75
76 if err := cfg.ParseEnv(); err != nil {
77 log.Fatal(err)
78 }
79
80 ctx = config.WithContext(ctx, cfg)
81 logger, f, err := newDefaultLogger(cfg)
82 if err != nil {
83 log.Errorf("failed to create logger: %v", err)
84 }
85
86 ctx = log.WithContext(ctx, logger)
87 if f != nil {
88 defer f.Close() // nolint: errcheck
89 }
90
91 // Set global logger
92 log.SetDefault(logger)
93
94 var opts []maxprocs.Option
95 if config.IsVerbose() {
96 opts = append(opts, maxprocs.Logger(log.Debugf))
97 }
98
99 // Set the max number of processes to the number of CPUs
100 // This is useful when running soft serve in a container
101 if _, err := maxprocs.Set(opts...); err != nil {
102 log.Warn("couldn't set automaxprocs", "error", err)
103 }
104
105 if err := rootCmd.ExecuteContext(ctx); err != nil {
106 os.Exit(1)
107 }
108}
109
110// newDefaultLogger returns a new logger with default settings.
111func newDefaultLogger(cfg *config.Config) (*log.Logger, *os.File, error) {
112 logger := log.NewWithOptions(os.Stderr, log.Options{
113 ReportTimestamp: true,
114 TimeFormat: time.DateOnly,
115 })
116
117 switch {
118 case config.IsVerbose():
119 logger.SetReportCaller(true)
120 fallthrough
121 case config.IsDebug():
122 logger.SetLevel(log.DebugLevel)
123 }
124
125 logger.SetTimeFormat(cfg.Log.TimeFormat)
126
127 switch strings.ToLower(cfg.Log.Format) {
128 case "json":
129 logger.SetFormatter(log.JSONFormatter)
130 case "logfmt":
131 logger.SetFormatter(log.LogfmtFormatter)
132 case "text":
133 logger.SetFormatter(log.TextFormatter)
134 }
135
136 var f *os.File
137 if cfg.Log.Path != "" {
138 f, err := os.OpenFile(cfg.Log.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
139 if err != nil {
140 return nil, nil, err
141 }
142 logger.SetOutput(f)
143 }
144
145 return logger, f, nil
146}
147
148func initBackendContext(cmd *cobra.Command, _ []string) error {
149 ctx := cmd.Context()
150 cfg := config.FromContext(ctx)
151 if _, err := os.Stat(cfg.DataPath); errors.Is(err, fs.ErrNotExist) {
152 if err := os.MkdirAll(cfg.DataPath, os.ModePerm); err != nil {
153 return fmt.Errorf("create data directory: %w", err)
154 }
155 }
156 dbx, err := db.Open(ctx, cfg.DB.Driver, cfg.DB.DataSource)
157 if err != nil {
158 return fmt.Errorf("open database: %w", err)
159 }
160
161 ctx = db.WithContext(ctx, dbx)
162 dbstore := database.New(ctx, dbx)
163 ctx = store.WithContext(ctx, dbstore)
164 be := backend.New(ctx, cfg, dbx)
165 ctx = backend.WithContext(ctx, be)
166
167 cmd.SetContext(ctx)
168
169 return nil
170}
171
172func closeDBContext(cmd *cobra.Command, _ []string) error {
173 ctx := cmd.Context()
174 dbx := db.FromContext(ctx)
175 if dbx != nil {
176 if err := dbx.Close(); err != nil {
177 return fmt.Errorf("close database: %w", err)
178 }
179 }
180
181 return nil
182}