Federated via graft
0

Configure Feed

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

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