root.go

  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}