all repos

mugit @ b8831a7

🐮 git server that your cow will love

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

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