Federated via graft
3.1 kB
110 lines
1package sync
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "fmt"
7
8 "graft/internal/forgejo"
9 "graft/internal/radicle"
10 "graft/internal/state"
11)
12
13// A marker prepended to mirrored items' bodies so a human browsing either
14// side can tell where an issue/patch actually originated.
15const mirrorNotice = "\n\n---\n_Mirrored automatically, do not edit here — edit at the source instead._"
16
17// IssueSyncer mirrors issues between one Forgejo repository and one Radicle
18// repository, create-only (see package doc for what that means).
19type IssueSyncer struct {
20 RepoPair string
21 Forgejo *forgejo.Client
22 Radicle *radicle.Client
23 State *state.Store
24 ForgejoWebURL string // https://host/owner/repo, for links in the UI
25 RadicleWebURL string // https://explorer/nodes/host/rid, for links in the UI
26}
27
28func (s *IssueSyncer) Sync() error {
29 forgejoIssues, err := s.Forgejo.ListIssues()
30 if err != nil {
31 return fmt.Errorf("list forgejo issues: %w", err)
32 }
33 radicleIssues, err := s.Radicle.ListIssues()
34 if err != nil {
35 return fmt.Errorf("list radicle issues: %w", err)
36 }
37
38 for _, fi := range forgejoIssues {
39 if err := s.mirrorForgejoToRadicle(fi); err != nil {
40 return fmt.Errorf("mirror forgejo issue #%d: %w", fi.Index, err)
41 }
42 }
43 for _, ri := range radicleIssues {
44 if err := s.mirrorRadicleToForgejo(ri); err != nil {
45 return fmt.Errorf("mirror radicle issue %s: %w", ri.ID, err)
46 }
47 }
48 return nil
49}
50
51func (s *IssueSyncer) mirrorForgejoToRadicle(fi forgejo.Issue) error {
52 existing, err := s.State.FindByForgejoID(s.RepoPair, "issue", fi.Index)
53 if err != nil {
54 return err
55 }
56 if existing != nil {
57 return nil // already mirrored; edits not yet synced (see package doc)
58 }
59
60 radicleID, err := s.Radicle.OpenIssue(fi.Title, fi.Body+mirrorNotice)
61 if err != nil {
62 return fmt.Errorf("open radicle issue: %w", err)
63 }
64
65 if err := s.State.Upsert(s.RepoPair, state.ItemMapping{
66 Kind: "issue",
67 ForgejoID: fi.Index,
68 RadicleID: radicleID,
69 ContentHash: hashText(fi.Title, fi.Body),
70 }); err != nil {
71 return err
72 }
73 s.State.LogActivity(s.RepoPair, "issue", state.ForgejoToRadicle, fi.Title, s.RadicleWebURL+"/issues/"+radicleID)
74 return nil
75}
76
77func (s *IssueSyncer) mirrorRadicleToForgejo(ri radicle.Issue) error {
78 existing, err := s.State.FindByRadicleID(s.RepoPair, "issue", ri.ID)
79 if err != nil {
80 return err
81 }
82 if existing != nil {
83 return nil
84 }
85
86 fi, err := s.Forgejo.CreateIssue(ri.Title, ri.Body()+mirrorNotice)
87 if err != nil {
88 return fmt.Errorf("create forgejo issue: %w", err)
89 }
90
91 if err := s.State.Upsert(s.RepoPair, state.ItemMapping{
92 Kind: "issue",
93 ForgejoID: fi.Index,
94 RadicleID: ri.ID,
95 ContentHash: hashText(ri.Title, ri.Body()),
96 }); err != nil {
97 return err
98 }
99 s.State.LogActivity(s.RepoPair, "issue", state.RadicleToForgejo, ri.Title, fmt.Sprintf("%s/issues/%d", s.ForgejoWebURL, fi.Index))
100 return nil
101}
102
103func hashText(parts ...string) string {
104 h := sha256.New()
105 for _, p := range parts {
106 h.Write([]byte(p))
107 h.Write([]byte{0})
108 }
109 return hex.EncodeToString(h.Sum(nil))
110}