Federated via graft
2.6 kB
100 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 return 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 })
69}
70
71func (s *IssueSyncer) mirrorRadicleToForgejo(ri radicle.Issue) error {
72 existing, err := s.State.FindByRadicleID(s.RepoPair, "issue", ri.ID)
73 if err != nil {
74 return err
75 }
76 if existing != nil {
77 return nil
78 }
79
80 fi, err := s.Forgejo.CreateIssue(ri.Title, ri.Body()+mirrorNotice)
81 if err != nil {
82 return fmt.Errorf("create forgejo issue: %w", err)
83 }
84
85 return s.State.Upsert(s.RepoPair, state.ItemMapping{
86 Kind: "issue",
87 ForgejoID: fi.Index,
88 RadicleID: ri.ID,
89 ContentHash: hashText(ri.Title, ri.Body()),
90 })
91}
92
93func hashText(parts ...string) string {
94 h := sha256.New()
95 for _, p := range parts {
96 h.Write([]byte(p))
97 h.Write([]byte{0})
98 }
99 return hex.EncodeToString(h.Sum(nil))
100}