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