Federated via graft
0

Configure Feed

Select the types of activity you want to include in your feed.

graft-source / internal / sync / syncer.go
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}