root.go

  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}