Federated via graft
0

Configure Feed

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

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