all repos

mugit @ a02c4e8b021b8808df85e7e1e2a09e9673ea2d17

🐮 git server that your cow will love

mugit/internal/mirror/mirror.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
test: add missing tests (#5)..., 2 months ago
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
func IsRemoteSupported(remote string) error {
20
	if !strings.HasPrefix(remote, "http") {
21
		return fmt.Errorf("only http and https remotes are supported")
22
	}
23
	return nil
24
}
25
26
func IsGithubRemote(remoteURL string) bool {
27
	return strings.Contains(remoteURL, "github.com")
28
}
29
30
type Worker struct {
31
	c *config.Config
32
}
33
34
func NewWorker(cfg *config.Config) *Worker {
35
	return &Worker{
36
		c: cfg,
37
	}
38
}
39
40
func (w *Worker) Start(ctx context.Context) error {
41
	ticker := time.NewTicker(w.c.Mirror.Interval)
42
	defer ticker.Stop()
43
44
	for {
45
		select {
46
		case <-ctx.Done():
47
			return nil
48
		default:
49
			if err := w.mirror(ctx); err != nil {
50
				slog.Error("mirror sync failed", "err", err)
51
			}
52
53
			<-ticker.C
54
		}
55
	}
56
}
57
58
func (w *Worker) mirror(ctx context.Context) error {
59
	repos, err := w.findMirrorRepos()
60
	if err != nil {
61
		return err
62
	}
63
64
	var wg sync.WaitGroup
65
	sem := semaphore.NewWeighted(10)
66
	errCh := make(chan error, len(repos))
67
68
	for _, repo := range repos {
69
		wg.Go(func() {
70
			if err := sem.Acquire(ctx, 1); err != nil {
71
				errCh <- err
72
				return
73
			}
74
			defer sem.Release(1)
75
76
			if err := w.syncRepo(ctx, repo); err != nil {
77
				errCh <- err
78
			}
79
		})
80
	}
81
82
	wg.Wait()
83
	close(errCh)
84
85
	var errs []error
86
	for err := range errCh {
87
		errs = append(errs, err)
88
	}
89
	return errors.Join(errs...)
90
}
91
92
func (w *Worker) syncRepo(ctx context.Context, repo *git.Repo) error {
93
	name := repo.Name()
94
	slog.Info("mirror: sync started", "repo", name)
95
96
	remoteURL, err := repo.RemoteURL()
97
	if err != nil {
98
		slog.Error("mirror: failed to get remote url", "repo", name, "err", err)
99
		return err
100
	}
101
102
	if err := IsRemoteSupported(remoteURL); err != nil {
103
		slog.Error("mirror: remote is not valid", "repo", name, "err", err)
104
		return err
105
	}
106
107
	if IsGithubRemote(remoteURL) && w.c.Mirror.GithubToken != "" {
108
		if err := repo.FetchFromGithubWithToken(ctx, w.c.Mirror.GithubToken); err != nil {
109
			slog.Error("mirror: fetch failed (github)", "repo", name, "err", err)
110
			return err
111
		}
112
	} else {
113
		if err := repo.Fetch(ctx); err != nil {
114
			slog.Error("mirror: fetch failed", "repo", name, "err", err)
115
			return err
116
		}
117
	}
118
119
	if err := repo.SetLastSync(time.Now()); err != nil {
120
		slog.Error("mirror: failed to set last sync time", "repo", name, "err", err)
121
	}
122
123
	slog.Info("mirror: sync completed", "repo", repo.Name())
124
	return nil
125
}
126
127
func (w *Worker) findMirrorRepos() ([]*git.Repo, error) {
128
	dirs, err := os.ReadDir(w.c.Repo.Dir)
129
	if err != nil {
130
		return nil, err
131
	}
132
133
	var repos []*git.Repo
134
	for _, dir := range dirs {
135
		if !dir.IsDir() {
136
			continue
137
		}
138
139
		name := dir.Name()
140
		path := filepath.Join(w.c.Repo.Dir, filepath.Clean(name))
141
		repo, err := git.Open(path, "")
142
		if err != nil {
143
			slog.Debug("skipping non-git directory", "name", name, "err", err)
144
			continue
145
		}
146
147
		isMirror, err := repo.IsMirror()
148
		if err != nil {
149
			slog.Debug("skipping non-mirror repo", "name", name, "err", err)
150
			continue
151
		}
152
153
		if isMirror {
154
			repos = append(repos, repo)
155
		}
156
	}
157
158
	return repos, nil
159
}