Federated via graft
4.0 kB
137 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 if pair.Sync.Git || pair.Sync.Patches {
48 rs.git = &GitSyncer{
49 RepoPair: pair.Name,
50 WorkDir: workDir,
51 ForgejoURL: authenticatedCloneURL(repo.CloneURL, token),
52 ForgejoWebURL: forgejoWebURL,
53 ForgejoBranch: repo.DefaultBranch,
54 RID: pair.Radicle.RID,
55 RadHome: pair.Radicle.RadHome,
56 State: st,
57 }
58 }
59 if pair.Sync.Issues {
60 rs.issues = &IssueSyncer{
61 RepoPair: pair.Name, Forgejo: fc, Radicle: rc, State: st,
62 ForgejoWebURL: forgejoWebURL, RadicleWebURL: radicleWebURL,
63 }
64 }
65 if pair.Sync.Patches {
66 rs.patches = &PatchSyncer{
67 RepoPair: pair.Name,
68 WorkDir: workDir,
69 RadHome: pair.Radicle.RadHome,
70 DefaultBranch: repo.DefaultBranch,
71 Forgejo: fc,
72 Radicle: rc,
73 State: st,
74 ForgejoWebURL: forgejoWebURL,
75 RadicleWebURL: radicleWebURL,
76 }
77 }
78 return rs, nil
79}
80
81// radicleExplorerLink builds the base URL for "view this on Radicle" links:
82// <explorer>/nodes/<httpd-host>/<rid>. Empty if no explorer_url is
83// configured, so templates can skip the link entirely.
84func radicleExplorerLink(rt config.RadicleTarget) string {
85 if rt.ExplorerURL == "" {
86 return ""
87 }
88 host := rt.HTTPBaseURL
89 if u, err := url.Parse(rt.HTTPBaseURL); err == nil && u.Host != "" {
90 host = u.Host
91 }
92 return strings.TrimRight(rt.ExplorerURL, "/") + "/nodes/" + host + "/" + rt.RID
93}
94
95// Name is this pair's name, as given in the config.
96func (rs *RepoSyncer) Name() string { return rs.pair.Name }
97
98// Run executes every enabled scope once, logging and continuing past
99// per-scope errors so one broken scope doesn't block the others. It
100// returns each scope's error (nil if disabled or successful) for the
101// caller to report via internal/status.
102func (rs *RepoSyncer) Run(log *slog.Logger) (gitErr, issuesErr, patchErr error) {
103 log = log.With("pair", rs.pair.Name)
104
105 if rs.git != nil {
106 if gitErr = rs.git.Sync(); gitErr != nil {
107 log.Error("git sync failed", "err", gitErr)
108 }
109 }
110 if rs.issues != nil {
111 if issuesErr = rs.issues.Sync(); issuesErr != nil {
112 log.Error("issue sync failed", "err", issuesErr)
113 }
114 }
115 if rs.patches != nil {
116 if patchErr = rs.patches.Sync(); patchErr != nil {
117 log.Error("patch sync failed", "err", patchErr)
118 }
119 }
120 return gitErr, issuesErr, patchErr
121}
122
123// authenticatedCloneURL embeds a token into an HTTPS clone URL the way
124// Forgejo expects (token as the basic-auth username).
125func authenticatedCloneURL(cloneURL, token string) string {
126 u, err := url.Parse(cloneURL)
127 if err != nil {
128 return cloneURL
129 }
130 u.User = url.User(token)
131 return u.String()
132}
133
134// WorkDirFor returns the per-pair git mirror directory under stateDir.
135func WorkDirFor(stateDir, pairName string) string {
136 return filepath.Join(stateDir, "repos", pairName)
137}