Federated via graft
3.2 kB
113 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// Name is this pair's name, as given in the config.
72func (rs *RepoSyncer) Name() string { return rs.pair.Name }
73
74// Run executes every enabled scope once, logging and continuing past
75// per-scope errors so one broken scope doesn't block the others. It
76// returns each scope's error (nil if disabled or successful) for the
77// caller to report via internal/status.
78func (rs *RepoSyncer) Run(log *slog.Logger) (gitErr, issuesErr, patchErr error) {
79 log = log.With("pair", rs.pair.Name)
80
81 if rs.git != nil {
82 if gitErr = rs.git.Sync(); gitErr != nil {
83 log.Error("git sync failed", "err", gitErr)
84 }
85 }
86 if rs.issues != nil {
87 if issuesErr = rs.issues.Sync(); issuesErr != nil {
88 log.Error("issue sync failed", "err", issuesErr)
89 }
90 }
91 if rs.patches != nil {
92 if patchErr = rs.patches.Sync(); patchErr != nil {
93 log.Error("patch sync failed", "err", patchErr)
94 }
95 }
96 return gitErr, issuesErr, patchErr
97}
98
99// authenticatedCloneURL embeds a token into an HTTPS clone URL the way
100// Forgejo expects (token as the basic-auth username).
101func authenticatedCloneURL(cloneURL, token string) string {
102 u, err := url.Parse(cloneURL)
103 if err != nil {
104 return cloneURL
105 }
106 u.User = url.User(token)
107 return u.String()
108}
109
110// WorkDirFor returns the per-pair git mirror directory under stateDir.
111func WorkDirFor(stateDir, pairName string) string {
112 return filepath.Join(stateDir, "repos", pairName)
113}