1package main
2
3import (
4 "context"
5 "encoding/json"
6 "io"
7 "os"
8 "path/filepath"
9 "runtime/debug"
10
11 "github.com/charmbracelet/log"
12 "github.com/charmbracelet/soft-serve/internal/logger"
13 "github.com/charmbracelet/soft-serve/server/access"
14 _ "github.com/charmbracelet/soft-serve/server/access/sqlite" // access driver
15 "github.com/charmbracelet/soft-serve/server/auth"
16 _ "github.com/charmbracelet/soft-serve/server/auth/sqlite" // auth driver
17 "github.com/charmbracelet/soft-serve/server/backend"
18 "github.com/charmbracelet/soft-serve/server/cache"
19 "github.com/charmbracelet/soft-serve/server/cache/lru"
20 _ "github.com/charmbracelet/soft-serve/server/cache/lru" // cache driver
21 _ "github.com/charmbracelet/soft-serve/server/cache/noop" // cache driver
22 "github.com/charmbracelet/soft-serve/server/config"
23 "github.com/charmbracelet/soft-serve/server/db"
24 _ "github.com/charmbracelet/soft-serve/server/db/sqlite" // db driver
25 "github.com/charmbracelet/soft-serve/server/settings"
26 _ "github.com/charmbracelet/soft-serve/server/settings/sqlite" // settings driver
27 "github.com/charmbracelet/soft-serve/server/store"
28 _ "github.com/charmbracelet/soft-serve/server/store/sqlite" // store driver
29 "github.com/go-git/go-billy/v5/osfs"
30 "github.com/spf13/cobra"
31 "go.uber.org/automaxprocs/maxprocs"
32)
33
34var (
35 // Version contains the application version number. It's set via ldflags
36 // when building.
37 Version = ""
38
39 // CommitSHA contains the SHA of the commit that this application was built
40 // against. It's set via ldflags when building.
41 CommitSHA = ""
42
43 configPath string
44
45 ojson bool
46
47 rootCmd = &cobra.Command{
48 Use: "soft",
49 Short: "A self-hostable Git server for the command line",
50 Long: "Soft Serve is a self-hostable Git server for the command line.",
51 SilenceUsage: true,
52 }
53)
54
55func init() {
56 rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "path to config file")
57 rootCmd.AddCommand(
58 serveCmd,
59 manCmd,
60 hookCmd,
61 migrateConfig,
62 authCmd,
63 uiCmd,
64 )
65
66 for _, cmd := range []*cobra.Command{
67 authCmd,
68 } {
69 cmd.PersistentFlags().BoolVar(&ojson, "json", false, "output as JSON")
70 }
71
72 rootCmd.CompletionOptions.HiddenDefaultCmd = true
73
74 if len(CommitSHA) >= 7 {
75 vt := rootCmd.VersionTemplate()
76 rootCmd.SetVersionTemplate(vt[:len(vt)-1] + " (" + CommitSHA[0:7] + ")\n")
77 }
78 if Version == "" {
79 if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
80 Version = info.Main.Version
81 } else {
82 Version = "unknown (built from source)"
83 }
84 }
85 rootCmd.Version = Version
86}
87
88func main() {
89 ctx := context.Background()
90
91 cfg := config.DefaultConfig()
92 if configPath != "" {
93 var err error
94 if err = config.ParseConfig(cfg, configPath); err != nil {
95 log.Fatal(err)
96 }
97 } else if !cfg.Exist() {
98 // Write config to disk.
99 if err := cfg.WriteConfig(); err != nil {
100 log.Fatalf("write default config: %w", err)
101 }
102 } else {
103 if err := cfg.ReadConfig(); err != nil {
104 log.Fatalf("read config: %w", err)
105 }
106 }
107
108 ctx = config.WithContext(ctx, cfg)
109 logger := logger.NewDefaultLogger(ctx)
110
111 // Set global logger
112 log.SetDefault(logger)
113
114 // Set the max number of processes to the number of CPUs
115 // This is useful when running soft serve in a container
116 if _, err := maxprocs.Set(maxprocs.Logger(log.Debugf)); err != nil {
117 log.Warn("couldn't set automaxprocs", "error", err)
118 }
119
120 ctx = log.WithContext(ctx, logger)
121
122 // Set up cache
123 var cacheOpts []cache.Option
124 cacheBackend := "noop"
125 switch cfg.Cache.Backend {
126 case "lru":
127 // TODO: make this configurable
128 cacheOpts = append(cacheOpts, lru.WithSize(1000))
129 cacheBackend = "lru"
130 }
131
132 ca, err := cache.New(ctx, cacheBackend, cacheOpts...)
133 if err != nil {
134 log.Fatalf("create default cache: %w", err)
135 }
136
137 ctx = cache.WithContext(ctx, ca)
138
139 // FIXME: move this somewhere and make order not required
140 // Set up database
141 sdb, err := db.New(ctx, cfg.Database.Driver, cfg.Database.DataSource)
142 if err != nil {
143 log.Fatalf("create sqlite database: %w", err)
144 }
145
146 ctx = db.WithContext(ctx, sdb)
147
148 // Set up auth backend.
149 a, err := auth.New(ctx, cfg.Backend.Auth)
150 if err != nil {
151 log.Fatalf("create auth backend: %w", err)
152 }
153
154 ctx = auth.WithContext(ctx, a)
155
156 // Set up store backend
157 fs := osfs.New(filepath.Join(cfg.DataPath, "repos"))
158 st, err := store.New(ctx, fs, cfg.Backend.Store)
159 if err != nil {
160 log.Fatalf("create store backend: %w", err)
161 }
162
163 ctx = store.WithContext(ctx, st)
164
165 // Set up settings backend.
166 s, err := settings.New(ctx, cfg.Backend.Settings)
167 if err != nil {
168 log.Fatalf("create settings backend: %w", err)
169 }
170
171 ctx = settings.WithContext(ctx, s)
172
173 // Set up access backend.
174 ac, err := access.New(ctx, cfg.Backend.Access)
175 if err != nil {
176 log.Fatalf("create access backend: %w", err)
177 }
178
179 ctx = access.WithContext(ctx, ac)
180
181 // Set up backend
182 be, err := backend.NewBackend(ctx, s, st, a, ac)
183 if err != nil {
184 log.Fatalf("create sqlite backend: %w", err)
185 }
186
187 ctx = backend.WithContext(ctx, be)
188
189 if rootCmd.ExecuteContext(ctx) != nil {
190 os.Exit(1)
191 }
192}
193
194func writeJSON(w io.Writer, t any) error {
195 bts, err := json.Marshal(t)
196 if err != nil {
197 return err
198 }
199 _, err = w.Write(bts)
200 return err
201}