Federated via graft
2.8 kB
108 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}
25
26func (s *IssueSyncer) Sync() error {
27 forgejoIssues, err := s.Forgejo.ListIssues()
28 if err != nil {
29 return fmt.Errorf("list forgejo issues: %w", err)
30 }
31 radicleIssues, err := s.Radicle.ListIssues()
32 if err != nil {
33 return fmt.Errorf("list radicle issues: %w", err)
34 }
35
36 for _, fi := range forgejoIssues {
37 if err := s.mirrorForgejoToRadicle(fi); err != nil {
38 return fmt.Errorf("mirror forgejo issue #%d: %w", fi.Index, err)
39 }
40 }
41 for _, ri := range radicleIssues {
42 if err := s.mirrorRadicleToForgejo(ri); err != nil {
43 return fmt.Errorf("mirror radicle issue %s: %w", ri.ID, err)
44 }
45 }
46 return nil
47}
48
49func (s *IssueSyncer) mirrorForgejoToRadicle(fi forgejo.Issue) error {
50 existing, err := s.State.FindByForgejoID(s.RepoPair, "issue", fi.Index)
51 if err != nil {
52 return err
53 }
54 if existing != nil {
55 return nil // already mirrored; edits not yet synced (see package doc)
56 }
57
58 radicleID, err := s.Radicle.OpenIssue(fi.Title, fi.Body+mirrorNotice)
59 if err != nil {
60 return fmt.Errorf("open radicle issue: %w", err)
61 }
62
63 if err := s.State.Upsert(s.RepoPair, state.ItemMapping{
64 Kind: "issue",
65 ForgejoID: fi.Index,
66 RadicleID: radicleID,
67 ContentHash: hashText(fi.Title, fi.Body),
68 }); err != nil {
69 return err
70 }
71 s.State.LogActivity(s.RepoPair, "issue", state.ForgejoToRadicle, fi.Title)
72 return nil
73}
74
75func (s *IssueSyncer) mirrorRadicleToForgejo(ri radicle.Issue) error {
76 existing, err := s.State.FindByRadicleID(s.RepoPair, "issue", ri.ID)
77 if err != nil {
78 return err
79 }
80 if existing != nil {
81 return nil
82 }
83
84 fi, err := s.Forgejo.CreateIssue(ri.Title, ri.Body()+mirrorNotice)
85 if err != nil {
86 return fmt.Errorf("create forgejo issue: %w", err)
87 }
88
89 if err := s.State.Upsert(s.RepoPair, state.ItemMapping{
90 Kind: "issue",
91 ForgejoID: fi.Index,
92 RadicleID: ri.ID,
93 ContentHash: hashText(ri.Title, ri.Body()),
94 }); err != nil {
95 return err
96 }
97 s.State.LogActivity(s.RepoPair, "issue", state.RadicleToForgejo, ri.Title)
98 return nil
99}
100
101func hashText(parts ...string) string {
102 h := sha256.New()
103 for _, p := range parts {
104 h.Write([]byte(p))
105 h.Write([]byte{0})
106 }
107 return hex.EncodeToString(h.Sum(nil))
108}