all repos

mugit @ 9d3f4dd63057f8e61e6bd90be32e6fbedf1d599b

🐮 git server that your cow will love

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

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