all repos

mugit @ d00a3f1

🐮 git server that your cow will love

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
mirror: sync on mirror repo creation, 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) SyncRepo(ctx context.Context, name string) error {
59
	path, err := git.ResolvePath(w.c.Repo.Dir, git.ResolveName(name))
60
	if err != nil {
61
		return fmt.Errorf("failed to resolve repo path: %w", err)
62
	}
63
64
	repo, err := git.Open(path, "")
65
	if err != nil {
66
		return fmt.Errorf("failed to open repo: %w", err)
67
	}
68
69
	isMirror, err := repo.IsMirror()
70
	if err != nil {
71
		return fmt.Errorf("failed to check mirror status: %w", err)
72
	}
73
	if !isMirror {
74
		return fmt.Errorf("repository is not a mirror")
75
	}
76
77
	return w.syncRepo(ctx, repo)
78
}
79
80
func (w *Worker) mirror(ctx context.Context) error {
81
	repos, err := w.findMirrorRepos()
82
	if err != nil {
83
		return err
84
	}
85
86
	var wg sync.WaitGroup
87
	sem := semaphore.NewWeighted(10)
88
	errCh := make(chan error, len(repos))
89
90
	for _, repo := range repos {
91
		wg.Go(func() {
92
			if err := sem.Acquire(ctx, 1); err != nil {
93
				errCh <- err
94
				return
95
			}
96
			defer sem.Release(1)
97
98
			if err := w.syncRepo(ctx, repo); err != nil {
99
				errCh <- err
100
			}
101
		})
102
	}
103
104
	wg.Wait()
105
	close(errCh)
106
107
	var errs []error
108
	for err := range errCh {
109
		errs = append(errs, err)
110
	}
111
	return errors.Join(errs...)
112
}
113
114
func (w *Worker) syncRepo(ctx context.Context, repo *git.Repo) error {
115
	name := repo.Name()
116
	slog.Info("mirror: sync started", "repo", name)
117
118
	remoteURL, err := repo.RemoteURL()
119
	if err != nil {
120
		slog.Error("mirror: failed to get remote url", "repo", name, "err", err)
121
		return err
122
	}
123
124
	if err := IsRemoteSupported(remoteURL); err != nil {
125
		slog.Error("mirror: remote is not valid", "repo", name, "err", err)
126
		return err
127
	}
128
129
	if IsGithubRemote(remoteURL) && w.c.Mirror.GithubToken != "" {
130
		if err := repo.FetchFromGithubWithToken(ctx, w.c.Mirror.GithubToken); err != nil {
131
			slog.Error("mirror: fetch failed (github)", "repo", name, "err", err)
132
			return err
133
		}
134
	} else {
135
		if err := repo.Fetch(ctx); err != nil {
136
			slog.Error("mirror: fetch failed", "repo", name, "err", err)
137
			return err
138
		}
139
	}
140
141
	if err := repo.SetLastSync(time.Now()); err != nil {
142
		slog.Error("mirror: failed to set last sync time", "repo", name, "err", err)
143
	}
144
145
	slog.Info("mirror: sync completed", "repo", repo.Name())
146
	return nil
147
}
148
149
func (w *Worker) findMirrorRepos() ([]*git.Repo, error) {
150
	dirs, err := os.ReadDir(w.c.Repo.Dir)
151
	if err != nil {
152
		return nil, err
153
	}
154
155
	var repos []*git.Repo
156
	for _, dir := range dirs {
157
		if !dir.IsDir() {
158
			continue
159
		}
160
161
		name := dir.Name()
162
		path := filepath.Join(w.c.Repo.Dir, filepath.Clean(name))
163
		repo, err := git.Open(path, "")
164
		if err != nil {
165
			slog.Debug("skipping non-git directory", "name", name, "err", err)
166
			continue
167
		}
168
169
		isMirror, err := repo.IsMirror()
170
		if err != nil {
171
			slog.Debug("skipping non-mirror repo", "name", name, "err", err)
172
			continue
173
		}
174
175
		if isMirror {
176
			repos = append(repos, repo)
177
		}
178
	}
179
180
	return repos, nil
181
}