1package main
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io/fs"
8 "os"
9 "runtime/debug"
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 logr "github.com/charmbracelet/soft-serve/server/log"
16 "github.com/charmbracelet/soft-serve/server/store"
17 "github.com/charmbracelet/soft-serve/server/store/database"
18 "github.com/charmbracelet/soft-serve/server/version"
19 "github.com/spf13/cobra"
20 "go.uber.org/automaxprocs/maxprocs"
21)
22
23var (
24 // Version contains the application version number. It's set via ldflags
25 // when building.
26 Version = ""
27
28 // CommitSHA contains the SHA of the commit that this application was built
29 // against. It's set via ldflags when building.
30 CommitSHA = ""
31
32 // CommitDate contains the date of the commit that this application was
33 // built against. It's set via ldflags when building.
34 CommitDate = ""
35
36 rootCmd = &cobra.Command{
37 Use: "soft",
38 Short: "A self-hostable Git server for the command line",
39 Long: "Soft Serve is a self-hostable Git server for the command line.",
40 SilenceUsage: true,
41 RunE: func(cmd *cobra.Command, args []string) error {
42 return browseCmd.RunE(cmd, args)
43 },
44 }
45)
46
47func init() {
48 rootCmd.AddCommand(
49 serveCmd,
50 manCmd,
51 hookCmd,
52 migrateConfig,
53 adminCmd,
54 )
55 rootCmd.CompletionOptions.HiddenDefaultCmd = true
56
57 if len(CommitSHA) >= 7 {
58 vt := rootCmd.VersionTemplate()
59 rootCmd.SetVersionTemplate(vt[:len(vt)-1] + " (" + CommitSHA[0:7] + ")\n")
60 }
61 if Version == "" {
62 if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
63 Version = info.Main.Version
64 } else {
65 Version = "unknown (built from source)"
66 }
67 }
68 rootCmd.Version = Version
69
70 version.Version = Version
71 version.CommitSHA = CommitSHA
72 version.CommitDate = CommitDate
73}
74
75func main() {
76 ctx := context.Background()
77 cfg := config.DefaultConfig()
78 if cfg.Exist() {
79 if err := cfg.Parse(); err != nil {
80 log.Fatal(err)
81 }
82 }
83
84 if err := cfg.ParseEnv(); err != nil {
85 log.Fatal(err)
86 }
87
88 ctx = config.WithContext(ctx, cfg)
89 logger, f, err := logr.NewLogger(cfg)
90 if err != nil {
91 log.Errorf("failed to create logger: %v", err)
92 }
93
94 ctx = log.WithContext(ctx, logger)
95 if f != nil {
96 defer f.Close() // nolint: errcheck
97 }
98
99 // Set global logger
100 log.SetDefault(logger)
101
102 var opts []maxprocs.Option
103 if config.IsVerbose() {
104 opts = append(opts, maxprocs.Logger(log.Debugf))
105 }
106
107 // Set the max number of processes to the number of CPUs
108 // This is useful when running soft serve in a container
109 if _, err := maxprocs.Set(opts...); err != nil {
110 log.Warn("couldn't set automaxprocs", "error", err)
111 }
112
113 if err := rootCmd.ExecuteContext(ctx); err != nil {
114 os.Exit(1)
115 }
116}
117
118func initBackendContext(cmd *cobra.Command, _ []string) error {
119 ctx := cmd.Context()
120 cfg := config.FromContext(ctx)
121 if _, err := os.Stat(cfg.DataPath); errors.Is(err, fs.ErrNotExist) {
122 if err := os.MkdirAll(cfg.DataPath, os.ModePerm); err != nil {
123 return fmt.Errorf("create data directory: %w", err)
124 }
125 }
126 dbx, err := db.Open(ctx, cfg.DB.Driver, cfg.DB.DataSource)
127 if err != nil {
128 return fmt.Errorf("open database: %w", err)
129 }
130
131 ctx = db.WithContext(ctx, dbx)
132 dbstore := database.New(ctx, dbx)
133 ctx = store.WithContext(ctx, dbstore)
134 be := backend.New(ctx, cfg, dbx)
135 ctx = backend.WithContext(ctx, be)
136
137 cmd.SetContext(ctx)
138
139 return nil
140}
141
142func closeDBContext(cmd *cobra.Command, _ []string) error {
143 ctx := cmd.Context()
144 dbx := db.FromContext(ctx)
145 if dbx != nil {
146 if err := dbx.Close(); err != nil {
147 return fmt.Errorf("close database: %w", err)
148 }
149 }
150
151 return nil
152}