all repos

mugit @ c328fa945ba477f410e888f13738d4c77dda5d57

馃惍 git server that your cow will love
8 files changed, 296 insertions(+), 38 deletions(-)
repo mirroring
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-01-22 01:48:03 +0200
Authored at: 2026-01-21 02:03:02 +0200
Change ID: knnxsuoupszlmxyuvkqnsrkokuqmknoy
Parent: 2efe21e
D config.yml
路路路
        1
        
        -server:

      
        2
        
        -  host: 0.0.0.0

      
        3
        
        -  port: 8008

      
        4
        
        -

      
        5
        
        -meta:

      
        6
        
        -  description: hey kid, come get your free software

      
        7
        
        -  title: git.olexsmir.xyz

      
        8
        
        -  host: git.olexsmir.xyz

      
        9
        
        -

      
        10
        
        -ssh:

      
        11
        
        -  enable: true

      
        12
        
        -  port: 2222

      
        13
        
        -  host_key: /home/olex/.ssh/mugit

      
        14
        
        -  keys:

      
        15
        
        -    - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPLLJdkVYKZgsayw+sHanKPKZbI0RMS2CakqBCEi5Trz

      
        16
        
        -    - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMPQ0Qz0DFB+rGrD8ScUqbUTZ1/O8FHrOBF5bIAGQgMj olex@rachlinux

      
        17
        
        -

      
        18
        
        -repo:

      
        19
        
        -  dir: /home/olex/mugit-test/

      
        20
        
        -  readmes:

      
        21
        
        -    - README

      
        22
        
        -    - README.md

      
        23
        
        -    - readme

      
        24
        
        -    - readme.md

      
        25
        
        -    - readme.txt

      
        26
        
        -  masters:

      
        27
        
        -    - master

      
        28
        
        -    - main

      
M go.mod
路路路
        8
        8
         	github.com/go-git/go-git/v5 v5.16.4

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

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

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

      
        11
        12
         	gopkg.in/yaml.v2 v2.4.0

      
        12
        13
         )

      
        13
        14
         

      
M go.sum
路路路
        79
        79
         golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=

      
        80
        80
         golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=

      
        81
        81
         golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=

      
        
        82
        +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=

      
        
        83
        +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=

      
        82
        84
         golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

      
        83
        85
         golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

      
        84
        86
         golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

      
M internal/config/config.go
路路路
        18
        18
         		Description string `yaml:"description"`

      
        19
        19
         		Host        string `yaml:"host"`

      
        20
        20
         	} `yaml:"meta"`

      
        
        21
        +	Repo struct {

      
        
        22
        +		Dir     string   `yaml:"dir"`

      
        
        23
        +		Readmes []string `yaml:"readmes"`

      
        
        24
        +		Masters []string `yaml:"masters"`

      
        
        25
        +	} `yaml:"repo"`

      
        21
        26
         	SSH struct {

      
        22
        27
         		Enable  bool     `yaml:"enable"`

      
        23
        28
         		Port    int      `yaml:"port"`

      
        24
        29
         		HostKey string   `yaml:"host_key"`

      
        25
        30
         		Keys    []string `yaml:"keys"`

      
        26
        31
         	} `yaml:"ssh"`

      
        27
        
        -	Repo struct {

      
        28
        
        -		Dir     string   `yaml:"dir"`

      
        29
        
        -		Readmes []string `yaml:"readmes"`

      
        30
        
        -		Masters []string `yaml:"masters"`

      
        31
        
        -	} `yaml:"repo"`

      
        
        32
        +	Mirror struct {

      
        
        33
        +		Enable      bool   `yaml:"enable"`

      
        
        34
        +		Interval    string `yaml:"interval"`

      
        
        35
        +		GithubToken string `yaml:"github_token"`

      
        
        36
        +	} `yaml:"mirror"`

      
        32
        37
         }

      
        33
        38
         

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

      
M internal/git/repo.go
路路路
        1
        1
         package git

      
        2
        2
         

      
        3
        3
         import (

      
        
        4
        +	"errors"

      
        4
        5
         	"fmt"

      
        5
        6
         	"os"

      
        6
        7
         	"path/filepath"

      
        7
        8
         	"sort"

      
        
        9
        +	"time"

      
        8
        10
         

      
        9
        11
         	"github.com/go-git/go-git/v5"

      
        
        12
        +	gitconfig "github.com/go-git/go-git/v5/config"

      
        10
        13
         	"github.com/go-git/go-git/v5/plumbing"

      
        11
        14
         	"github.com/go-git/go-git/v5/plumbing/object"

      
        
        15
        +	"github.com/go-git/go-git/v5/plumbing/transport/http"

      
        12
        16
         )

      
        13
        17
         

      
        14
        18
         // Thanks https://git.icyphox.sh/legit/blob/master/git/git.go

      路路路
        43
        47
         		g.h = *hash

      
        44
        48
         	}

      
        45
        49
         	return &g, nil

      
        
        50
        +}

      
        
        51
        +

      
        
        52
        +func (g *Repo) Name() string {

      
        
        53
        +	return filepath.Base(g.path)

      
        46
        54
         }

      
        47
        55
         

      
        48
        56
         func (g *Repo) Commits() ([]*object.Commit, error) {

      路路路
        180
        188
         	}

      
        181
        189
         	return "", fmt.Errorf("unable to find master branch")

      
        182
        190
         }

      
        
        191
        +

      
        
        192
        +type MirrorInfo struct {

      
        
        193
        +	IsMirror  bool

      
        
        194
        +	Remote    string

      
        
        195
        +	RemoteURL string

      
        
        196
        +}

      
        
        197
        +

      
        
        198
        +func (g *Repo) MirrorInfo() (MirrorInfo, error) {

      
        
        199
        +	c, err := g.r.Config()

      
        
        200
        +	if err != nil {

      
        
        201
        +		return MirrorInfo{}, fmt.Errorf("failed to read config: %w", err)

      
        
        202
        +	}

      
        
        203
        +

      
        
        204
        +	isMirror := c.Raw.Section("mugit").Options.Get("mirror") == "true"

      
        
        205
        +	for _, remote := range c.Remotes {

      
        
        206
        +		if len(remote.URLs) > 0 && (remote.Name == "upstream" || remote.Name == "origin") {

      
        
        207
        +			return MirrorInfo{

      
        
        208
        +				IsMirror:  isMirror,

      
        
        209
        +				Remote:    remote.Name,

      
        
        210
        +				RemoteURL: remote.URLs[0],

      
        
        211
        +			}, nil

      
        
        212
        +		}

      
        
        213
        +	}

      
        
        214
        +	// TODO: error if mirror opt is set, but there's no remotes

      
        
        215
        +	return MirrorInfo{}, fmt.Errorf("no mirror remote found")

      
        
        216
        +}

      
        
        217
        +

      
        
        218
        +func (g *Repo) ReadLastSync() (time.Time, error) {

      
        
        219
        +	c, err := g.r.Config()

      
        
        220
        +	if err != nil {

      
        
        221
        +		return time.Time{}, fmt.Errorf("failed to read config: %w", err)

      
        
        222
        +	}

      
        
        223
        +

      
        
        224
        +	raw := c.Raw.Section("mugit").Options.Get("last-sync")

      
        
        225
        +	if raw == "" {

      
        
        226
        +		return time.Time{}, fmt.Errorf("last-sync not set")

      
        
        227
        +	}

      
        
        228
        +

      
        
        229
        +	out, err := time.Parse(time.RFC3339, string(raw))

      
        
        230
        +	if err != nil {

      
        
        231
        +		return time.Time{}, fmt.Errorf("failed to parse time: %w", err)

      
        
        232
        +	}

      
        
        233
        +	return out, nil

      
        
        234
        +}

      
        
        235
        +

      
        
        236
        +func (g *Repo) SetLastSync(lastSync time.Time) error {

      
        
        237
        +	c, err := g.r.Config()

      
        
        238
        +	if err != nil {

      
        
        239
        +		return fmt.Errorf("failed to read config: %w", err)

      
        
        240
        +	}

      
        
        241
        +

      
        
        242
        +	c.Raw.Section("mugit").

      
        
        243
        +		SetOption("last-sync", lastSync.Format(time.RFC3339))

      
        
        244
        +	return g.r.SetConfig(c)

      
        
        245
        +}

      
        
        246
        +

      
        
        247
        +func (g *Repo) Fetch(remote string) error {

      
        
        248
        +	return g.FetchWithAuth(remote, "")

      
        
        249
        +}

      
        
        250
        +

      
        
        251
        +// FetchWithAuth fetches but with auth. Works only with github's auth

      
        
        252
        +func (g *Repo) FetchWithAuth(remote string, token string) error {

      
        
        253
        +	rmt, err := g.r.Remote(remote)

      
        
        254
        +	if err != nil {

      
        
        255
        +		return fmt.Errorf("failed to get upstream remote: %w", err)

      
        
        256
        +	}

      
        
        257
        +

      
        
        258
        +	opts := &git.FetchOptions{

      
        
        259
        +		RefSpecs: []gitconfig.RefSpec{

      
        
        260
        +			// fetch all branches

      
        
        261
        +			"+refs/heads/*:refs/heads/*",

      
        
        262
        +			"+refs/tags/*:refs/tags/*",

      
        
        263
        +		},

      
        
        264
        +		Tags:  git.AllTags,

      
        
        265
        +		Prune: true,

      
        
        266
        +		Force: true,

      
        
        267
        +	}

      
        
        268
        +

      
        
        269
        +	if token != "" {

      
        
        270
        +		opts.Auth = &http.BasicAuth{

      
        
        271
        +			Username: token,

      
        
        272
        +			Password: "x-oauth-basic",

      
        
        273
        +		}

      
        
        274
        +	}

      
        
        275
        +

      
        
        276
        +	if err := rmt.Fetch(opts); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {

      
        
        277
        +		return fmt.Errorf("fetch failed: %w", err)

      
        
        278
        +	}

      
        
        279
        +	return nil

      
        
        280
        +}

      
M internal/handlers/repo.go
路路路
        34
        34
         

      
        35
        35
         	repoInfos := []repoInfo{}

      
        36
        36
         	for _, dir := range dirs {

      
        
        37
        +		if !dir.IsDir() {

      
        
        38
        +			continue

      
        
        39
        +		}

      
        
        40
        +

      
        37
        41
         		name := dir.Name()

      
        38
        42
         		repo, err := h.openPublicRepo(name, "")

      
        39
        43
         		if err != nil {

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

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"errors"

      
        
        6
        +	"fmt"

      
        
        7
        +	"log/slog"

      
        
        8
        +	"os"

      
        
        9
        +	"path/filepath"

      
        
        10
        +	"strings"

      
        
        11
        +	"sync"

      
        
        12
        +	"time"

      
        
        13
        +

      
        
        14
        +	"golang.org/x/sync/semaphore"

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

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

      
        
        17
        +)

      
        
        18
        +

      
        
        19
        +type Worker struct {

      
        
        20
        +	c *config.Config

      
        
        21
        +}

      
        
        22
        +

      
        
        23
        +func NewWorker(cfg *config.Config) *Worker {

      
        
        24
        +	return &Worker{

      
        
        25
        +		c: cfg,

      
        
        26
        +	}

      
        
        27
        +}

      
        
        28
        +

      
        
        29
        +func (w *Worker) Start(ctx context.Context) error {

      
        
        30
        +	interval, err := time.ParseDuration(w.c.Mirror.Interval)

      
        
        31
        +	if err != nil {

      
        
        32
        +		slog.Error("couldn't parse interval time", "err", err)

      
        
        33
        +		return err

      
        
        34
        +	}

      
        
        35
        +

      
        
        36
        +	ticker := time.NewTicker(interval)

      
        
        37
        +	defer ticker.Stop()

      
        
        38
        +

      
        
        39
        +	if err := w.mirror(ctx); err != nil {

      
        
        40
        +		slog.Error("initial mirror sync failed", "err", err)

      
        
        41
        +	}

      
        
        42
        +

      
        
        43
        +	for {

      
        
        44
        +		select {

      
        
        45
        +		case <-ctx.Done():

      
        
        46
        +			return nil

      
        
        47
        +		case <-ticker.C:

      
        
        48
        +			if err := w.mirror(ctx); err != nil {

      
        
        49
        +				slog.Error("mirror sync failed", "err", err)

      
        
        50
        +			}

      
        
        51
        +		}

      
        
        52
        +	}

      
        
        53
        +}

      
        
        54
        +

      
        
        55
        +func (w *Worker) mirror(ctx context.Context) error {

      
        
        56
        +	repos, err := w.findMirrorRepos()

      
        
        57
        +	if err != nil {

      
        
        58
        +		return err

      
        
        59
        +	}

      
        
        60
        +

      
        
        61
        +	var wg sync.WaitGroup

      
        
        62
        +	sem := semaphore.NewWeighted(5)

      
        
        63
        +	errCh := make(chan error, len(repos))

      
        
        64
        +

      
        
        65
        +	for _, repo := range repos {

      
        
        66
        +		wg.Go(func() {

      
        
        67
        +			if err := sem.Acquire(ctx, 1); err != nil {

      
        
        68
        +				errCh <- err

      
        
        69
        +				return

      
        
        70
        +			}

      
        
        71
        +			defer sem.Release(1)

      
        
        72
        +

      
        
        73
        +			if err := w.syncRepo(ctx, repo); err != nil {

      
        
        74
        +				errCh <- err

      
        
        75
        +			}

      
        
        76
        +		})

      
        
        77
        +	}

      
        
        78
        +	wg.Wait()

      
        
        79
        +	close(errCh)

      
        
        80
        +

      
        
        81
        +	var errs []error

      
        
        82
        +	for err := range errCh {

      
        
        83
        +		errs = append(errs, err)

      
        
        84
        +	}

      
        
        85
        +	return errors.Join(errs...)

      
        
        86
        +}

      
        
        87
        +

      
        
        88
        +func (w *Worker) syncRepo(_ context.Context, repo *git.Repo) error {

      
        
        89
        +	name := repo.Name()

      
        
        90
        +	slog.Info("mirror: sync started", "repo", name)

      
        
        91
        +

      
        
        92
        +	mi, err := repo.MirrorInfo()

      
        
        93
        +	if err != nil {

      
        
        94
        +		slog.Error("mirror: failed to get info", "repo", name, "err", err)

      
        
        95
        +		return err

      
        
        96
        +	}

      
        
        97
        +

      
        
        98
        +	if err := w.isRemoteValid(mi.RemoteURL); err != nil {

      
        
        99
        +		slog.Error("mirror: remote is not valid", "repo", name, "err", err)

      
        
        100
        +		return err

      
        
        101
        +	}

      
        
        102
        +

      
        
        103
        +	if w.isRemoteGithub(mi.RemoteURL) && w.c.Mirror.GithubToken != "" {

      
        
        104
        +		if err := repo.FetchWithAuth(mi.Remote, w.c.Mirror.GithubToken); err != nil {

      
        
        105
        +			slog.Error("mirror: fetch failed (authorized)", "repo", name, "err", err)

      
        
        106
        +			return err

      
        
        107
        +		}

      
        
        108
        +	} else {

      
        
        109
        +		if err := repo.Fetch(mi.Remote); err != nil {

      
        
        110
        +			slog.Error("mirror: fetch failed", "repo", name, "err", err)

      
        
        111
        +			return err

      
        
        112
        +		}

      
        
        113
        +	}

      
        
        114
        +

      
        
        115
        +	if err := repo.SetLastSync(time.Now()); err != nil {

      
        
        116
        +		slog.Error("mirror: failed to set last sync time", "repo", name, "err", err)

      
        
        117
        +	}

      
        
        118
        +

      
        
        119
        +	slog.Info("mirror: sync completed", "repo", repo.Name())

      
        
        120
        +	return nil

      
        
        121
        +}

      
        
        122
        +

      
        
        123
        +func (w *Worker) findMirrorRepos() ([]*git.Repo, error) {

      
        
        124
        +	dirs, err := os.ReadDir(w.c.Repo.Dir)

      
        
        125
        +	if err != nil {

      
        
        126
        +		return nil, err

      
        
        127
        +	}

      
        
        128
        +

      
        
        129
        +	var repos []*git.Repo

      
        
        130
        +	for _, dir := range dirs {

      
        
        131
        +		if !dir.IsDir() {

      
        
        132
        +			continue

      
        
        133
        +		}

      
        
        134
        +

      
        
        135
        +		name := dir.Name()

      
        
        136
        +		repo, err := git.Open(filepath.Join(w.c.Repo.Dir, filepath.Clean(name)), "")

      
        
        137
        +		if err != nil {

      
        
        138
        +			slog.Debug("skipping non-git directory", "path", name, "err", err)

      
        
        139
        +			continue

      
        
        140
        +		}

      
        
        141
        +

      
        
        142
        +		mirror, err := repo.MirrorInfo()

      
        
        143
        +		if err != nil {

      
        
        144
        +			slog.Debug("skipping non-mirror repo", "path", name, "err", err)

      
        
        145
        +			continue

      
        
        146
        +		}

      
        
        147
        +

      
        
        148
        +		if mirror.IsMirror {

      
        
        149
        +			repos = append(repos, repo)

      
        
        150
        +		}

      
        
        151
        +	}

      
        
        152
        +

      
        
        153
        +	return repos, nil

      
        
        154
        +}

      
        
        155
        +

      
        
        156
        +func (w *Worker) isRemoteValid(remote string) error {

      
        
        157
        +	if !strings.HasPrefix(remote, "http") {

      
        
        158
        +		return fmt.Errorf("only http and https remotes are supported")

      
        
        159
        +	}

      
        
        160
        +	return nil

      
        
        161
        +}

      
        
        162
        +

      
        
        163
        +func (w *Worker) isRemoteGithub(remoteURL string) bool {

      
        
        164
        +	return strings.Contains(remoteURL, "github.com")

      
        
        165
        +}

      
M main.go
路路路
        13
        13
         

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

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

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

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

      
        17
        18
         )

      
        18
        19
         

      路路路
        24
        25
         }

      
        25
        26
         

      
        26
        27
         func run() error {

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

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

      
        28
        29
         	if err != nil {

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

      
        30
        31
         		return err

      路路路
        45
        46
         

      
        46
        47
         	sshServer := ssh.NewServer(cfg)

      
        47
        48
         	if cfg.SSH.Enable {

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

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

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

      
        51
        
        -		}

      
        
        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
        +		}()

      
        52
        63
         	}

      
        53
        64
         

      
        54
        65
         	// Wait for interrupt signal