5 files changed,
150 insertions(+),
71 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-01-22 23:27:17 +0200
Change ID:
ovqyknqtnunrsvwqttqzxoxosrwpzull
Parent:
c328fa9
jump to
| M | go.mod |
| M | go.sum |
| A | internal/cli/cli.go |
| M | internal/config/config.go |
| M | main.go |
M
go.mod
@@ -6,6 +6,7 @@ require (
github.com/bluekeyes/go-gitdiff v0.8.1 github.com/gliderlabs/ssh v0.3.8 github.com/go-git/go-git/v5 v5.16.4 + github.com/urfave/cli/v3 v3.6.2 github.com/yuin/goldmark v1.7.16 golang.org/x/crypto v0.47.0 golang.org/x/sync v0.19.0
M
go.sum
@@ -65,8 +65,10 @@ github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8= +github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
A
internal/cli/cli.go
@@ -0,0 +1,102 @@
+package cli + +import ( + "context" + "log/slog" + "net" + "net/http" + "os" + "os/signal" + "strconv" + "syscall" + + "github.com/urfave/cli/v3" + "olexsmir.xyz/mugit/internal/config" + "olexsmir.xyz/mugit/internal/handlers" + "olexsmir.xyz/mugit/internal/mirror" + "olexsmir.xyz/mugit/internal/ssh" +) + +type Cli struct { + cfg *config.Config +} + +func New() *Cli { + return &Cli{} +} + +func (c *Cli) Run(ctx context.Context, args []string) error { + cmd := &cli.Command{ + Name: "mugit", + Usage: "a frontend for git repos", + EnableShellCompletion: true, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config", + Aliases: []string{"c"}, + Usage: "path to config file", + }, + }, + Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) { + loadedCfg, err := config.Load(cmd.String("config")) + if err != nil { + return ctx, err + } + c.cfg = loadedCfg + return ctx, nil + }, + Commands: []*cli.Command{ + { + Name: "serve", + Usage: "starts the server", + Action: c.serveAction, + }, + }, + } + return cmd.Run(ctx, args) +} + +func (c *Cli) serveAction(ctx context.Context, cmd *cli.Command) error { + httpServer := &http.Server{ + Addr: net.JoinHostPort(c.cfg.Server.Host, strconv.Itoa(c.cfg.Server.Port)), + Handler: handlers.InitRoutes(c.cfg), + } + go func() { + slog.Info("starting http server", "host", c.cfg.Server.Host, "port", c.cfg.Server.Port) + if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + slog.Error("HTTP server error", "err", err) + } + }() + + if c.cfg.SSH.Enable { + sshServer := ssh.NewServer(c.cfg) + go func() { + slog.Info("starting ssh server", "port", c.cfg.SSH.Port) + if err := sshServer.Start(); err != nil { + slog.Error("ssh server error", "err", err) + } + }() + } + + if c.cfg.Mirror.Enable { + mirrorer := mirror.NewWorker(c.cfg) + go func() { + slog.Info("starting mirroring worker") + mirrorer.Start(context.TODO()) + }() + } + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + sig := <-sigChan + slog.Info("received signal, starting graceful shutdown", "signal", sig) + + if err := httpServer.Shutdown(context.TODO()); err != nil { + slog.Error("HTTP server shutdown error", "err", err) + } else { + slog.Info("HTTP server shutdown complete") + } + + return nil +}
M
internal/config/config.go
@@ -1,12 +1,15 @@
package config import ( + "errors" "fmt" "os" "path/filepath" "gopkg.in/yaml.v2" ) + +var ErrConfigNotFound = errors.New("no config file found") type Config struct { Server struct {@@ -36,13 +39,23 @@ GithubToken string `yaml:"github_token"`
} `yaml:"mirror"` } +// Load loads configuration with the following priority: +// 1. User provided fpath (if provided and exists) +// 2. $XDG_CONFIG_HOME/mugit/config.yaml +// 3. $HOME/.config/mugit/config.yaml (fallback if XDG_CONFIG_HOME not set) +// 4. /etc/mugit/config.yaml func Load(fpath string) (*Config, error) { - configBytes, err := os.ReadFile(fpath) + configPath, err := findConfigFile(fpath) if err != nil { return nil, err } - var config *Config + configBytes, err := os.ReadFile(configPath) + if err != nil { + return nil, err + } + + var config Config if cerr := yaml.Unmarshal(configBytes, &config); cerr != nil { return nil, fmt.Errorf("parsing config: %w", cerr) }@@ -55,7 +68,7 @@ if verr := config.validate(); verr != nil {
return nil, verr } - return config, nil + return &config, nil } func (c Config) validate() error {@@ -63,3 +76,27 @@ // var errs []error
// return errors.Join(errs...) return nil } + +func findConfigFile(userPath string) (string, error) { + if userPath != "" { + if _, err := os.Stat(userPath); err == nil { + return userPath, nil + } + } + + paths := []string{} + if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" { + paths = append(paths, filepath.Join(xdg, "mugit", "config.yaml")) + } else if home := os.Getenv("HOME"); home != "" { + paths = append(paths, filepath.Join(home, ".config", "mugit", "config.yaml")) + } + + paths = append(paths, "/etc/mugit/config.yaml") + for _, p := range paths { + if _, err := os.Stat(p); err == nil { + return p, nil + } + } + + return "", ErrConfigNotFound +}
M
main.go
@@ -2,78 +2,15 @@ package main
import ( "context" - "log" "log/slog" - "net" - "net/http" "os" - "os/signal" - "strconv" - "syscall" - "olexsmir.xyz/mugit/internal/config" - "olexsmir.xyz/mugit/internal/handlers" - "olexsmir.xyz/mugit/internal/mirror" - "olexsmir.xyz/mugit/internal/ssh" + "olexsmir.xyz/mugit/internal/cli" ) func main() { - if err := run(); err != nil { - log.Fatalf("main: %s", err) + if err := cli.New().Run(context.TODO(), os.Args); err != nil { + slog.Error("mugit", "err", err) os.Exit(1) } } - -func run() error { - cfg, err := config.Load("/home/olex/mugit-test/config.yml") - if err != nil { - slog.Error("config error", "err", err) - return err - } - - httpServer := &http.Server{ - Addr: net.JoinHostPort(cfg.Server.Host, strconv.Itoa(cfg.Server.Port)), - Handler: handlers.InitRoutes(cfg), - } - - go func() { - slog.Info("starting http server", "host", cfg.Server.Host, "port", cfg.Server.Port) - - if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - slog.Error("HTTP server error", "err", err) - } - }() - - sshServer := ssh.NewServer(cfg) - if cfg.SSH.Enable { - go func() { - slog.Info("starting ssh server", "port", cfg.SSH.Port) - if err := sshServer.Start(); err != nil { - slog.Error("ssh server error", "err", err) - } - }() - } - - mirrorer := mirror.NewWorker(cfg) - if cfg.Mirror.Enable { - go func() { - slog.Info("starting mirroring worker") - mirrorer.Start(context.TODO()) - }() - } - - // Wait for interrupt signal - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - sig := <-sigChan - slog.Info("received signal, starting graceful shutdown", "signal", sig) - - if err := httpServer.Shutdown(context.TODO()); err != nil { - slog.Error("HTTP server shutdown error", "err", err) - } else { - slog.Info("HTTP server shutdown complete") - } - - return nil -}