Federated via graft
4.6 kB
161 lines
1package sync
2
3import (
4 "fmt"
5 "log/slog"
6 "net/url"
7 "path/filepath"
8 "strings"
9
10 "graft/internal/config"
11 "graft/internal/forgejo"
12 "graft/internal/radicle"
13 "graft/internal/state"
14)
15
16// RepoSyncer runs one repo pair's enabled sync scopes for one pass.
17type RepoSyncer struct {
18 pair config.RepoPair
19 forgejo *forgejo.Client
20 radicle *radicle.Client
21 git *GitSyncer
22 issues *IssueSyncer
23 patches *PatchSyncer
24}
25
26// New builds a RepoSyncer for one configured pair. workDir is where this
27// pair's local git mirror is kept, e.g. <state-dir>/<pair-name>.
28func New(pair config.RepoPair, st *state.Store, workDir string) (*RepoSyncer, error) {
29 token, err := config.ReadToken(pair.Forgejo.TokenFile)
30 if err != nil {
31 return nil, err
32 }
33 fc := forgejo.New(pair.Forgejo.BaseURL, pair.Forgejo.Owner, pair.Forgejo.Repo, token)
34
35 repo, err := fc.GetRepository()
36 if err != nil {
37 return nil, fmt.Errorf("resolve forgejo repository: %w", err)
38 }
39
40 rc := radicle.New(pair.Radicle.HTTPBaseURL, pair.Radicle.RID, pair.Radicle.RadHome, "rad")
41
42 rs := &RepoSyncer{pair: pair, forgejo: fc, radicle: rc}
43
44 forgejoWebURL := strings.TrimRight(pair.Forgejo.BaseURL, "/") + "/" + pair.Forgejo.Owner + "/" + pair.Forgejo.Repo
45 radicleWebURL := radicleExplorerLink(pair.Radicle)
46
47 series := pair.Series
48 if series == "" {
49 series = pair.Name
50 }
51 seriesURL := radicleWebURL
52 if seriesURL == "" {
53 seriesURL = forgejoWebURL
54 }
55
56 if pair.Sync.Git || pair.Sync.Patches {
57 rs.git = &GitSyncer{
58 RepoPair: pair.Name,
59 WorkDir: workDir,
60 ForgejoURL: authenticatedCloneURL(repo.CloneURL, token),
61 ForgejoWebURL: forgejoWebURL,
62 RadicleWebURL: radicleWebURL,
63 ForgejoBranch: repo.DefaultBranch,
64 RID: pair.Radicle.RID,
65 RadHome: pair.Radicle.RadHome,
66 State: st,
67 Series: series,
68 SeriesURL: seriesURL,
69 }
70 }
71 if pair.Sync.Issues {
72 rs.issues = &IssueSyncer{
73 RepoPair: pair.Name, Forgejo: fc, Radicle: rc, State: st,
74 ForgejoWebURL: forgejoWebURL, RadicleWebURL: radicleWebURL,
75 Series: series, SeriesURL: seriesURL,
76 }
77 }
78 if pair.Sync.Patches {
79 rs.patches = &PatchSyncer{
80 RepoPair: pair.Name,
81 WorkDir: workDir,
82 RadHome: pair.Radicle.RadHome,
83 DefaultBranch: repo.DefaultBranch,
84 Forgejo: fc,
85 Radicle: rc,
86 State: st,
87 ForgejoWebURL: forgejoWebURL,
88 RadicleWebURL: radicleWebURL,
89 Series: series,
90 SeriesURL: seriesURL,
91 }
92 }
93 return rs, nil
94}
95
96// radicleExplorerLink builds the base URL for "view this on Radicle" links:
97// <explorer>/nodes/<httpd-host>/<rid>. Empty if no explorer_url is
98// configured, so templates can skip the link entirely.
99func radicleExplorerLink(rt config.RadicleTarget) string {
100 if rt.ExplorerURL == "" {
101 return ""
102 }
103 host := rt.HTTPBaseURL
104 if u, err := url.Parse(rt.HTTPBaseURL); err == nil && u.Host != "" {
105 host = u.Host
106 }
107 return strings.TrimRight(rt.ExplorerURL, "/") + "/nodes/" + host + "/" + rt.RID
108}
109
110// Name is this pair's name, as given in the config.
111func (rs *RepoSyncer) Name() string { return rs.pair.Name }
112
113// Series is the dashboard row this pair's activity groups under: its
114// configured series, or its own name if unset.
115func (rs *RepoSyncer) Series() string {
116 if rs.pair.Series != "" {
117 return rs.pair.Series
118 }
119 return rs.pair.Name
120}
121
122// Run executes every enabled scope once, logging and continuing past
123// per-scope errors so one broken scope doesn't block the others. It
124// returns each scope's error (nil if disabled or successful) for the
125// caller to report via internal/status.
126func (rs *RepoSyncer) Run(log *slog.Logger) (gitErr, issuesErr, patchErr error) {
127 log = log.With("pair", rs.pair.Name)
128
129 if rs.git != nil {
130 if gitErr = rs.git.Sync(); gitErr != nil {
131 log.Error("git sync failed", "err", gitErr)
132 }
133 }
134 if rs.issues != nil {
135 if issuesErr = rs.issues.Sync(); issuesErr != nil {
136 log.Error("issue sync failed", "err", issuesErr)
137 }
138 }
139 if rs.patches != nil {
140 if patchErr = rs.patches.Sync(); patchErr != nil {
141 log.Error("patch sync failed", "err", patchErr)
142 }
143 }
144 return gitErr, issuesErr, patchErr
145}
146
147// authenticatedCloneURL embeds a token into an HTTPS clone URL the way
148// Forgejo expects (token as the basic-auth username).
149func authenticatedCloneURL(cloneURL, token string) string {
150 u, err := url.Parse(cloneURL)
151 if err != nil {
152 return cloneURL
153 }
154 u.User = url.User(token)
155 return u.String()
156}
157
158// WorkDirFor returns the per-pair git mirror directory under stateDir.
159func WorkDirFor(stateDir, pairName string) string {
160 return filepath.Join(stateDir, "repos", pairName)
161}