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.9 kB 131 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 radicleByID := make(map[string]radicle.Issue, len(radicleIssues)) 40 for _, ri := range radicleIssues { 41 radicleByID[ri.ID] = ri 42 } 43 44 for _, fi := range forgejoIssues { 45 if err := s.mirrorForgejoToRadicle(fi, radicleByID); err != nil { 46 return fmt.Errorf("mirror forgejo issue #%d: %w", fi.Index, err) 47 } 48 } 49 for _, ri := range radicleIssues { 50 if err := s.mirrorRadicleToForgejo(ri); err != nil { 51 return fmt.Errorf("mirror radicle issue %s: %w", ri.ID, err) 52 } 53 } 54 return nil 55} 56 57func (s *IssueSyncer) mirrorForgejoToRadicle(fi forgejo.Issue, radicleByID map[string]radicle.Issue) error { 58 existing, err := s.State.FindByForgejoID(s.RepoPair, "issue", fi.Index) 59 if err != nil { 60 return err 61 } 62 if existing != nil { 63 if ri, ok := radicleByID[existing.RadicleID]; ok { 64 if err := s.syncIssueComments(fi.Index, ri); err != nil { 65 return fmt.Errorf("sync comments: %w", err) 66 } 67 } 68 return nil // already mirrored; edits not yet synced (see package doc) 69 } 70 71 radicleID, err := s.Radicle.OpenIssue(fi.Title, fi.Body+mirrorNotice) 72 if err != nil { 73 return fmt.Errorf("open radicle issue: %w", err) 74 } 75 76 if err := s.State.Upsert(s.RepoPair, state.ItemMapping{ 77 Kind: "issue", 78 ForgejoID: fi.Index, 79 RadicleID: radicleID, 80 ContentHash: hashText(fi.Title, fi.Body), 81 }); err != nil { 82 return err 83 } 84 s.State.LogActivity(state.Activity{ 85 RepoPair: s.RepoPair, Series: s.Series, SeriesURL: s.SeriesURL, 86 Kind: "issue", Direction: state.ForgejoToRadicle, Summary: fi.Title, 87 URL: s.RadicleWebURL + "/issues/" + radicleID, 88 ForgejoID: fi.Index, RadicleID: radicleID, 89 }) 90 return nil 91} 92 93func (s *IssueSyncer) mirrorRadicleToForgejo(ri radicle.Issue) error { 94 existing, err := s.State.FindByRadicleID(s.RepoPair, "issue", ri.ID) 95 if err != nil { 96 return err 97 } 98 if existing != nil { 99 return nil 100 } 101 102 fi, err := s.Forgejo.CreateIssue(ri.Title, ri.Body()+mirrorNotice) 103 if err != nil { 104 return fmt.Errorf("create forgejo issue: %w", err) 105 } 106 107 if err := s.State.Upsert(s.RepoPair, state.ItemMapping{ 108 Kind: "issue", 109 ForgejoID: fi.Index, 110 RadicleID: ri.ID, 111 ContentHash: hashText(ri.Title, ri.Body()), 112 }); err != nil { 113 return err 114 } 115 s.State.LogActivity(state.Activity{ 116 RepoPair: s.RepoPair, Series: s.Series, SeriesURL: s.SeriesURL, 117 Kind: "issue", Direction: state.RadicleToForgejo, Summary: ri.Title, 118 URL: fmt.Sprintf("%s/issues/%d", s.ForgejoWebURL, fi.Index), 119 ForgejoID: fi.Index, RadicleID: ri.ID, 120 }) 121 return nil 122} 123 124func hashText(parts ...string) string { 125 h := sha256.New() 126 for _, p := range parts { 127 h.Write([]byte(p)) 128 h.Write([]byte{0}) 129 } 130 return hex.EncodeToString(h.Sum(nil)) 131}