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}