Federated via graft
2.9 kB
107 lines
1package sync
2
3import (
4 "fmt"
5 "log/slog"
6 "net/url"
7 "path/filepath"
8
9 "graft/internal/config"
10 "graft/internal/forgejo"
11 "graft/internal/radicle"
12 "graft/internal/state"
13)
14
15// RepoSyncer runs one repo pair's enabled sync scopes for one pass.
16type RepoSyncer struct {
17 pair config.RepoPair
18 forgejo *forgejo.Client
19 radicle *radicle.Client
20 git *GitSyncer
21 issues *IssueSyncer
22 patches *PatchSyncer
23}
24
25// New builds a RepoSyncer for one configured pair. workDir is where this
26// pair's local git mirror is kept, e.g. <state-dir>/<pair-name>.
27func New(pair config.RepoPair, st *state.Store, workDir string) (*RepoSyncer, error) {
28 token, err := config.ReadToken(pair.Forgejo.TokenFile)
29 if err != nil {
30 return nil, err
31 }
32 fc := forgejo.New(pair.Forgejo.BaseURL, pair.Forgejo.Owner, pair.Forgejo.Repo, token)
33
34 repo, err := fc.GetRepository()
35 if err != nil {
36 return nil, fmt.Errorf("resolve forgejo repository: %w", err)
37 }
38
39 rc := radicle.New(pair.Radicle.HTTPBaseURL, pair.Radicle.RID, pair.Radicle.RadHome, "rad")
40
41 rs := &RepoSyncer{pair: pair, forgejo: fc, radicle: rc}
42
43 if pair.Sync.Git || pair.Sync.Patches {
44 rs.git = &GitSyncer{
45 RepoPair: pair.Name,
46 WorkDir: workDir,
47 ForgejoURL: authenticatedCloneURL(repo.CloneURL, token),
48 ForgejoBranch: repo.DefaultBranch,
49 RID: pair.Radicle.RID,
50 RadHome: pair.Radicle.RadHome,
51 State: st,
52 }
53 }
54 if pair.Sync.Issues {
55 rs.issues = &IssueSyncer{RepoPair: pair.Name, Forgejo: fc, Radicle: rc, State: st}
56 }
57 if pair.Sync.Patches {
58 rs.patches = &PatchSyncer{
59 RepoPair: pair.Name,
60 WorkDir: workDir,
61 RadHome: pair.Radicle.RadHome,
62 DefaultBranch: repo.DefaultBranch,
63 Forgejo: fc,
64 Radicle: rc,
65 State: st,
66 }
67 }
68 return rs, nil
69}
70
71// Run executes every enabled scope once, logging and continuing past
72// per-scope errors so one broken scope doesn't block the others.
73func (rs *RepoSyncer) Run(log *slog.Logger) {
74 log = log.With("pair", rs.pair.Name)
75
76 if rs.git != nil {
77 if err := rs.git.Sync(); err != nil {
78 log.Error("git sync failed", "err", err)
79 }
80 }
81 if rs.issues != nil {
82 if err := rs.issues.Sync(); err != nil {
83 log.Error("issue sync failed", "err", err)
84 }
85 }
86 if rs.patches != nil {
87 if err := rs.patches.Sync(); err != nil {
88 log.Error("patch sync failed", "err", err)
89 }
90 }
91}
92
93// authenticatedCloneURL embeds a token into an HTTPS clone URL the way
94// Forgejo expects (token as the basic-auth username).
95func authenticatedCloneURL(cloneURL, token string) string {
96 u, err := url.Parse(cloneURL)
97 if err != nil {
98 return cloneURL
99 }
100 u.User = url.User(token)
101 return u.String()
102}
103
104// WorkDirFor returns the per-pair git mirror directory under stateDir.
105func WorkDirFor(stateDir, pairName string) string {
106 return filepath.Join(stateDir, "repos", pairName)
107}