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