all repos

mugit @ d4f69f4f508a8d4fd6e87585912b53a6bd2aea17

🐮 git server that your cow will love

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
cli: reuse common method for validating remote validity, 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
type Worker struct {
27
	c *config.Config
28
}
29
30
func NewWorker(cfg *config.Config) *Worker {
31
	return &Worker{
32
		c: cfg,
33
	}
34
}
35
36
func (w *Worker) Start(ctx context.Context) error {
37
	ticker := time.NewTicker(w.c.Mirror.Interval)
38
	defer ticker.Stop()
39
40
	for {
41
		select {
42
		case <-ctx.Done():
43
			return nil
44
		default:
45
			if err := w.mirror(ctx); err != nil {
46
				slog.Error("mirror sync failed", "err", err)
47
			}
48
49
			<-ticker.C
50
		}
51
	}
52
}
53
54
func (w *Worker) mirror(ctx context.Context) error {
55
	repos, err := w.findMirrorRepos()
56
	if err != nil {
57
		return err
58
	}
59
60
	var wg sync.WaitGroup
61
	sem := semaphore.NewWeighted(10)
62
	errCh := make(chan error, len(repos))
63
64
	for _, repo := range repos {
65
		wg.Go(func() {
66
			if err := sem.Acquire(ctx, 1); err != nil {
67
				errCh <- err
68
				return
69
			}
70
			defer sem.Release(1)
71
72
			if err := w.syncRepo(ctx, repo); err != nil {
73
				errCh <- err
74
			}
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(ctx context.Context, repo *git.Repo) error {
89
	name := repo.Name()
90
	slog.Info("mirror: sync started", "repo", name)
91
92
	remoteURL, err := repo.RemoteURL()
93
	if err != nil {
94
		slog.Error("mirror: failed to get remote url", "repo", name, "err", err)
95
		return err
96
	}
97
98
	if err := IsRemoteSupported(remoteURL); err != nil {
99
		slog.Error("mirror: remote is not valid", "repo", name, "err", err)
100
		return err
101
	}
102
103
	if w.isRemoteGithub(remoteURL) && w.c.Mirror.GithubToken != "" {
104
		if err := repo.FetchFromGithubWithToken(ctx, w.c.Mirror.GithubToken); err != nil {
105
			slog.Error("mirror: fetch failed (github)", "repo", name, "err", err)
106
			return err
107
		}
108
	} else {
109
		if err := repo.Fetch(ctx); 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
		path := filepath.Join(w.c.Repo.Dir, filepath.Clean(name))
137
		repo, err := git.Open(path, "")
138
		if err != nil {
139
			slog.Debug("skipping non-git directory", "name", name, "err", err)
140
			continue
141
		}
142
143
		isMirror, err := repo.IsMirror()
144
		if err != nil {
145
			slog.Debug("skipping non-mirror repo", "name", name, "err", err)
146
			continue
147
		}
148
149
		if isMirror {
150
			repos = append(repos, repo)
151
		}
152
	}
153
154
	return repos, nil
155
}
156
157
func (w *Worker) isRemoteGithub(remoteURL string) bool {
158
	return strings.Contains(remoteURL, "github.com")
159
}