all repos

mugit @ 10fc8ef

馃惍 git server that your cow will love
5 files changed, 150 insertions(+), 71 deletions(-)
start the cli work
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-01-22 23:27:17 +0200
Authored at: 2026-01-21 23:58:11 +0200
Change ID: ovqyknqtnunrsvwqttqzxoxosrwpzull
Parent: c328fa9
M go.mod
路路路
        6
        6
         	github.com/bluekeyes/go-gitdiff v0.8.1

      
        7
        7
         	github.com/gliderlabs/ssh v0.3.8

      
        8
        8
         	github.com/go-git/go-git/v5 v5.16.4

      
        
        9
        +	github.com/urfave/cli/v3 v3.6.2

      
        9
        10
         	github.com/yuin/goldmark v1.7.16

      
        10
        11
         	golang.org/x/crypto v0.47.0

      
        11
        12
         	golang.org/x/sync v0.19.0

      
M go.sum
路路路
        65
        65
         github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

      
        66
        66
         github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=

      
        67
        67
         github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=

      
        68
        
        -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=

      
        69
        
        -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=

      
        
        68
        +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=

      
        
        69
        +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=

      
        
        70
        +github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=

      
        
        71
        +github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=

      
        70
        72
         github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=

      
        71
        73
         github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=

      
        72
        74
         github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=

      
A internal/cli/cli.go
路路路
        
        1
        +package cli

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"log/slog"

      
        
        6
        +	"net"

      
        
        7
        +	"net/http"

      
        
        8
        +	"os"

      
        
        9
        +	"os/signal"

      
        
        10
        +	"strconv"

      
        
        11
        +	"syscall"

      
        
        12
        +

      
        
        13
        +	"github.com/urfave/cli/v3"

      
        
        14
        +	"olexsmir.xyz/mugit/internal/config"

      
        
        15
        +	"olexsmir.xyz/mugit/internal/handlers"

      
        
        16
        +	"olexsmir.xyz/mugit/internal/mirror"

      
        
        17
        +	"olexsmir.xyz/mugit/internal/ssh"

      
        
        18
        +)

      
        
        19
        +

      
        
        20
        +type Cli struct {

      
        
        21
        +	cfg *config.Config

      
        
        22
        +}

      
        
        23
        +

      
        
        24
        +func New() *Cli {

      
        
        25
        +	return &Cli{}

      
        
        26
        +}

      
        
        27
        +

      
        
        28
        +func (c *Cli) Run(ctx context.Context, args []string) error {

      
        
        29
        +	cmd := &cli.Command{

      
        
        30
        +		Name:                  "mugit",

      
        
        31
        +		Usage:                 "a frontend for git repos",

      
        
        32
        +		EnableShellCompletion: true,

      
        
        33
        +		Flags: []cli.Flag{

      
        
        34
        +			&cli.StringFlag{

      
        
        35
        +				Name:    "config",

      
        
        36
        +				Aliases: []string{"c"},

      
        
        37
        +				Usage:   "path to config file",

      
        
        38
        +			},

      
        
        39
        +		},

      
        
        40
        +		Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {

      
        
        41
        +			loadedCfg, err := config.Load(cmd.String("config"))

      
        
        42
        +			if err != nil {

      
        
        43
        +				return ctx, err

      
        
        44
        +			}

      
        
        45
        +			c.cfg = loadedCfg

      
        
        46
        +			return ctx, nil

      
        
        47
        +		},

      
        
        48
        +		Commands: []*cli.Command{

      
        
        49
        +			{

      
        
        50
        +				Name:   "serve",

      
        
        51
        +				Usage:  "starts the server",

      
        
        52
        +				Action: c.serveAction,

      
        
        53
        +			},

      
        
        54
        +		},

      
        
        55
        +	}

      
        
        56
        +	return cmd.Run(ctx, args)

      
        
        57
        +}

      
        
        58
        +

      
        
        59
        +func (c *Cli) serveAction(ctx context.Context, cmd *cli.Command) error {

      
        
        60
        +	httpServer := &http.Server{

      
        
        61
        +		Addr:    net.JoinHostPort(c.cfg.Server.Host, strconv.Itoa(c.cfg.Server.Port)),

      
        
        62
        +		Handler: handlers.InitRoutes(c.cfg),

      
        
        63
        +	}

      
        
        64
        +	go func() {

      
        
        65
        +		slog.Info("starting http server", "host", c.cfg.Server.Host, "port", c.cfg.Server.Port)

      
        
        66
        +		if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {

      
        
        67
        +			slog.Error("HTTP server error", "err", err)

      
        
        68
        +		}

      
        
        69
        +	}()

      
        
        70
        +

      
        
        71
        +	if c.cfg.SSH.Enable {

      
        
        72
        +		sshServer := ssh.NewServer(c.cfg)

      
        
        73
        +		go func() {

      
        
        74
        +			slog.Info("starting ssh server", "port", c.cfg.SSH.Port)

      
        
        75
        +			if err := sshServer.Start(); err != nil {

      
        
        76
        +				slog.Error("ssh server error", "err", err)

      
        
        77
        +			}

      
        
        78
        +		}()

      
        
        79
        +	}

      
        
        80
        +

      
        
        81
        +	if c.cfg.Mirror.Enable {

      
        
        82
        +		mirrorer := mirror.NewWorker(c.cfg)

      
        
        83
        +		go func() {

      
        
        84
        +			slog.Info("starting mirroring worker")

      
        
        85
        +			mirrorer.Start(context.TODO())

      
        
        86
        +		}()

      
        
        87
        +	}

      
        
        88
        +

      
        
        89
        +	sigChan := make(chan os.Signal, 1)

      
        
        90
        +	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

      
        
        91
        +

      
        
        92
        +	sig := <-sigChan

      
        
        93
        +	slog.Info("received signal, starting graceful shutdown", "signal", sig)

      
        
        94
        +

      
        
        95
        +	if err := httpServer.Shutdown(context.TODO()); err != nil {

      
        
        96
        +		slog.Error("HTTP server shutdown error", "err", err)

      
        
        97
        +	} else {

      
        
        98
        +		slog.Info("HTTP server shutdown complete")

      
        
        99
        +	}

      
        
        100
        +

      
        
        101
        +	return nil

      
        
        102
        +}

      
M internal/config/config.go
路路路
        1
        1
         package config

      
        2
        2
         

      
        3
        3
         import (

      
        
        4
        +	"errors"

      
        4
        5
         	"fmt"

      
        5
        6
         	"os"

      
        6
        7
         	"path/filepath"

      
        7
        8
         

      
        8
        9
         	"gopkg.in/yaml.v2"

      
        9
        10
         )

      
        
        11
        +

      
        
        12
        +var ErrConfigNotFound = errors.New("no config file found")

      
        10
        13
         

      
        11
        14
         type Config struct {

      
        12
        15
         	Server struct {

      路路路
        36
        39
         	} `yaml:"mirror"`

      
        37
        40
         }

      
        38
        41
         

      
        
        42
        +// Load loads configuration with the following priority:

      
        
        43
        +// 1. User provided fpath (if provided and exists)

      
        
        44
        +// 2. $XDG_CONFIG_HOME/mugit/config.yaml

      
        
        45
        +// 3. $HOME/.config/mugit/config.yaml (fallback if XDG_CONFIG_HOME not set)

      
        
        46
        +// 4. /etc/mugit/config.yaml

      
        39
        47
         func Load(fpath string) (*Config, error) {

      
        40
        
        -	configBytes, err := os.ReadFile(fpath)

      
        
        48
        +	configPath, err := findConfigFile(fpath)

      
        41
        49
         	if err != nil {

      
        42
        50
         		return nil, err

      
        43
        51
         	}

      
        44
        52
         

      
        45
        
        -	var config *Config

      
        
        53
        +	configBytes, err := os.ReadFile(configPath)

      
        
        54
        +	if err != nil {

      
        
        55
        +		return nil, err

      
        
        56
        +	}

      
        
        57
        +

      
        
        58
        +	var config Config

      
        46
        59
         	if cerr := yaml.Unmarshal(configBytes, &config); cerr != nil {

      
        47
        60
         		return nil, fmt.Errorf("parsing config: %w", cerr)

      
        48
        61
         	}

      路路路
        55
        68
         		return nil, verr

      
        56
        69
         	}

      
        57
        70
         

      
        58
        
        -	return config, nil

      
        
        71
        +	return &config, nil

      
        59
        72
         }

      
        60
        73
         

      
        61
        74
         func (c Config) validate() error {

      路路路
        63
        76
         	// return errors.Join(errs...)

      
        64
        77
         	return nil

      
        65
        78
         }

      
        
        79
        +

      
        
        80
        +func findConfigFile(userPath string) (string, error) {

      
        
        81
        +	if userPath != "" {

      
        
        82
        +		if _, err := os.Stat(userPath); err == nil {

      
        
        83
        +			return userPath, nil

      
        
        84
        +		}

      
        
        85
        +	}

      
        
        86
        +

      
        
        87
        +	paths := []string{}

      
        
        88
        +	if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {

      
        
        89
        +		paths = append(paths, filepath.Join(xdg, "mugit", "config.yaml"))

      
        
        90
        +	} else if home := os.Getenv("HOME"); home != "" {

      
        
        91
        +		paths = append(paths, filepath.Join(home, ".config", "mugit", "config.yaml"))

      
        
        92
        +	}

      
        
        93
        +

      
        
        94
        +	paths = append(paths, "/etc/mugit/config.yaml")

      
        
        95
        +	for _, p := range paths {

      
        
        96
        +		if _, err := os.Stat(p); err == nil {

      
        
        97
        +			return p, nil

      
        
        98
        +		}

      
        
        99
        +	}

      
        
        100
        +

      
        
        101
        +	return "", ErrConfigNotFound

      
        
        102
        +}

      
M main.go
路路路
        2
        2
         

      
        3
        3
         import (

      
        4
        4
         	"context"

      
        5
        
        -	"log"

      
        6
        5
         	"log/slog"

      
        7
        
        -	"net"

      
        8
        
        -	"net/http"

      
        9
        6
         	"os"

      
        10
        
        -	"os/signal"

      
        11
        
        -	"strconv"

      
        12
        
        -	"syscall"

      
        13
        7
         

      
        14
        
        -	"olexsmir.xyz/mugit/internal/config"

      
        15
        
        -	"olexsmir.xyz/mugit/internal/handlers"

      
        16
        
        -	"olexsmir.xyz/mugit/internal/mirror"

      
        17
        
        -	"olexsmir.xyz/mugit/internal/ssh"

      
        
        8
        +	"olexsmir.xyz/mugit/internal/cli"

      
        18
        9
         )

      
        19
        10
         

      
        20
        11
         func main() {

      
        21
        
        -	if err := run(); err != nil {

      
        22
        
        -		log.Fatalf("main: %s", err)

      
        
        12
        +	if err := cli.New().Run(context.TODO(), os.Args); err != nil {

      
        
        13
        +		slog.Error("mugit", "err", err)

      
        23
        14
         		os.Exit(1)

      
        24
        15
         	}

      
        25
        16
         }

      
        26
        
        -

      
        27
        
        -func run() error {

      
        28
        
        -	cfg, err := config.Load("/home/olex/mugit-test/config.yml")

      
        29
        
        -	if err != nil {

      
        30
        
        -		slog.Error("config error", "err", err)

      
        31
        
        -		return err

      
        32
        
        -	}

      
        33
        
        -

      
        34
        
        -	httpServer := &http.Server{

      
        35
        
        -		Addr:    net.JoinHostPort(cfg.Server.Host, strconv.Itoa(cfg.Server.Port)),

      
        36
        
        -		Handler: handlers.InitRoutes(cfg),

      
        37
        
        -	}

      
        38
        
        -

      
        39
        
        -	go func() {

      
        40
        
        -		slog.Info("starting http server", "host", cfg.Server.Host, "port", cfg.Server.Port)

      
        41
        
        -

      
        42
        
        -		if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {

      
        43
        
        -			slog.Error("HTTP server error", "err", err)

      
        44
        
        -		}

      
        45
        
        -	}()

      
        46
        
        -

      
        47
        
        -	sshServer := ssh.NewServer(cfg)

      
        48
        
        -	if cfg.SSH.Enable {

      
        49
        
        -		go func() {

      
        50
        
        -			slog.Info("starting ssh server", "port", cfg.SSH.Port)

      
        51
        
        -			if err := sshServer.Start(); err != nil {

      
        52
        
        -				slog.Error("ssh server error", "err", err)

      
        53
        
        -			}

      
        54
        
        -		}()

      
        55
        
        -	}

      
        56
        
        -

      
        57
        
        -	mirrorer := mirror.NewWorker(cfg)

      
        58
        
        -	if cfg.Mirror.Enable {

      
        59
        
        -		go func() {

      
        60
        
        -			slog.Info("starting mirroring worker")

      
        61
        
        -			mirrorer.Start(context.TODO())

      
        62
        
        -		}()

      
        63
        
        -	}

      
        64
        
        -

      
        65
        
        -	// Wait for interrupt signal

      
        66
        
        -	sigChan := make(chan os.Signal, 1)

      
        67
        
        -	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

      
        68
        
        -

      
        69
        
        -	sig := <-sigChan

      
        70
        
        -	slog.Info("received signal, starting graceful shutdown", "signal", sig)

      
        71
        
        -

      
        72
        
        -	if err := httpServer.Shutdown(context.TODO()); err != nil {

      
        73
        
        -		slog.Error("HTTP server shutdown error", "err", err)

      
        74
        
        -	} else {

      
        75
        
        -		slog.Info("HTTP server shutdown complete")

      
        76
        
        -	}

      
        77
        
        -

      
        78
        
        -	return nil

      
        79
        
        -}